diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac9ac77732cccc9edcdfded2e23a09d4ee1d5c07 --- /dev/null +++ b/app/finders/concerns/created_at_filter.rb @@ -0,0 +1,8 @@ +module CreatedAtFilter + def by_created_at(items) + items = items.created_before(params[:created_before]) if params[:created_before].present? + items = items.created_after(params[:created_after]) if params[:created_after].present? + + items + end +end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index d81e9ed17d48cb619ce547e90ef97165ebd9975f..2e5a6493134bf6afa43900065a54796d2a37a380 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,8 @@ # iids: integer[] # class IssuableFinder + include CreatedAtFilter + NONE = '0'.freeze IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze @@ -32,6 +34,7 @@ class IssuableFinder def execute items = init_collection items = by_scope(items) + items = by_created_at(items) items = by_state(items) items = by_group(items) items = by_search(items) @@ -42,7 +45,6 @@ class IssuableFinder items = by_iids(items) items = by_milestone(items) items = by_label(items) - items = by_created_at(items) # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far items = by_project(items) @@ -411,18 +413,6 @@ class IssuableFinder params[:non_archived].present? ? items.non_archived : items end - def by_created_at(items) - if params[:created_after].present? - items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after])) - end - - if params[:created_before].present? - items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before])) - end - - items - end - def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 07deceb827bb7999ac338b573729c8616eef73ed..33f7ae905987b0e415c3025af28c276ca8114254 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -14,6 +14,8 @@ # external: boolean # class UsersFinder + include CreatedAtFilter + attr_accessor :current_user, :params def initialize(current_user, params = {}) @@ -29,6 +31,7 @@ class UsersFinder users = by_active(users) users = by_external_identity(users) users = by_external(users) + users = by_created_at(users) users end diff --git a/app/models/concerns/created_at_filterable.rb b/app/models/concerns/created_at_filterable.rb new file mode 100644 index 0000000000000000000000000000000000000000..e8a3e41203d80599620a3c178ce529038034823e --- /dev/null +++ b/app/models/concerns/created_at_filterable.rb @@ -0,0 +1,12 @@ +module CreatedAtFilterable + extend ActiveSupport::Concern + + included do + scope :created_before, ->(date) { where(scoped_table[:created_at].lteq(date)) } + scope :created_after, ->(date) { where(scoped_table[:created_at].gteq(date)) } + + def self.scoped_table + arel_table.alias(table_name) + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 01f985823e14d0d3c4b301a6606c2d04c72f21a2..400bb55d2f0998b4c34317bf68dd9e1e344cfc62 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base include FasterCacheKeys include RelativePositioning include IgnorableColumn + include CreatedAtFilterable ignore_column :position @@ -50,8 +51,6 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } - scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } - scope :preload_associations, -> { preload(:labels, project: :namespace) } after_save :expire_etag_cache diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6ea774470afb5c6554992aa28b11502a0c54ec0c..30caa3598d1ca3fdea1da104f3e4e14bfc18fa2a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -5,6 +5,7 @@ class MergeRequest < ActiveRecord::Base include Referable include Sortable include IgnorableColumn + include CreatedAtFilterable ignore_column :position diff --git a/app/models/user.rb b/app/models/user.rb index 4411a06d429b45b9451616313646c6f2d5718765..4b01c2f19f079494c680a8260c02b74a759fa87c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,7 @@ class User < ActiveRecord::Base include TokenAuthenticatable include IgnorableColumn include FeatureGate + include CreatedAtFilterable DEFAULT_NOTIFICATION_LEVEL = :participating diff --git a/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml b/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml new file mode 100644 index 0000000000000000000000000000000000000000..27ac50c6cc268cab3230de26343290578a275279 --- /dev/null +++ b/changelogs/unreleased/feature-user-datetime-search-api-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Add creation time filters to user search API for admins +merge_request: 12682 +author: diff --git a/config/application.rb b/config/application.rb index d88740ef8d7327d1a62c698574c631da6228b385..2f4e26241951ffc6cda954f675a1461c86af8b50 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,7 +26,8 @@ module Gitlab #{config.root}/app/models/members #{config.root}/app/models/project_services #{config.root}/app/workers/concerns - #{config.root}/app/services/concerns)) + #{config.root}/app/services/concerns + #{config.root}/app/finders/concerns)) config.generators.templates.push("#{config.root}/generator_templates") diff --git a/doc/api/users.md b/doc/api/users.md index cf09b8f44aa3e528b448fe0d9ae6aeba54a95d24..91170e79645d192db0cf85513d514b265b70a7d6 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -146,6 +146,12 @@ GET /users?extern_uid=1234567&provider=github You can search for users who are external with: `/users?external=true` +You can search users by creation date time range with: + +``` +GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060 +``` + ## Single user Get a single user. diff --git a/lib/api/users.rb b/lib/api/users.rb index 88bca235692f66b10638ec0c6cf15e4057515c72..c469751c31c8419525e761f46177aba2fd74bec0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -48,6 +48,8 @@ module API optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' + optional :created_after, type: DateTime, desc: 'Return users created after the specified time' + optional :created_before, type: DateTime, desc: 'Return users created before the specified time' all_or_none_of :extern_uid, :provider use :pagination @@ -55,6 +57,10 @@ module API get do authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) + unless current_user&.admin? + params.except!(:created_after, :created_before) + end + users = UsersFinder.new(current_user, params).execute authorized = can?(current_user, :read_users_list) diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index 780b309b45e2305f5e685852cb04fc8af34c8536..1bab6d6438814e1d18e38492500ad2aee01fab7a 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -45,6 +45,17 @@ describe UsersFinder do expect(users).to contain_exactly(user, user1, user2, omniauth_user) end + + it 'filters by created_at' do + filtered_user_before = create(:user, created_at: 3.days.ago) + filtered_user_after = create(:user, created_at: Time.now + 3.days) + + users = described_class.new(user, + created_after: 2.days.ago, + created_before: Time.now + 2.days).execute + + expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username]) + end end context 'with an admin user' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 70b94a09e6b635fa7be234afb1958399c1ec6526..c34b88f07410b570c98fcc32b466042272351055 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -163,6 +163,35 @@ describe API::Users do expect(response).to have_http_status(400) end + + it "returns a user created before a specific date" do + user = create(:user, created_at: Date.new(2000, 1, 1)) + + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(user.username) + end + + it "returns no users created before a specific date" do + create(:user, created_at: Date.new(2001, 1, 1)) + + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + + it "returns users created before and after a specific date" do + user = create(:user, created_at: Date.new(2001, 1, 1)) + + get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['username']).to eq(user.username) + end end end