diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index a1969571a4de5d83e4d3f3f912892c8913747069..868d60b8f57ad3d7bb2db883583f7e76802657c7 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -78,4 +78,21 @@ $(() => { location.reload(); } }); + + $('.js-user-select').each((i, el) => { + const $tgt = $(el); + $tgt.select2({ + ajax: { + url: '/users/search', + headers: { 'Accept': 'application/json' }, + delay: 100, + data: function (params) { + return { + search: params.term + }; + }, + processResults: data => ({results: data.map(c => ({id: c.id, text: `${c.username} (${c.email})`}))}), + } + }); + }); }); \ No newline at end of file diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb index 021929e52d69c360ace3e3aa01a31e8b2aaaecea..6f93964c9cdef0a1b3d71c575152db221d8a099b 100644 --- a/app/controllers/badges_controller.rb +++ b/app/controllers/badges_controller.rb @@ -53,6 +53,49 @@ class BadgesController < ApplicationController end end + def award + @entity_types = badge_source_types + @user_badge = UserBadge.new + end + + def award_save + @entity_types = badge_source_types + @user = User.find_by(id: params[:user_badge][:user]) + @badge = Badge.find_by(id: params[:user_badge][:badge]) + @user_badge = UserBadge.new(params.require(:user_badge).permit(:badge_type, :badge_source_type, + :badge_source_id) + .merge(user: @user, badge: @badge, + badge_type: params[:user_badge][:badge_type].to_i)) + + unless badge_source_types.include? @user_badge.badge_source_type + flash[:danger] = 'Post Type is invalid.' + redirect_to :award, status: :bad_request + return + end + + case @user_badge.badge_source_type + when 'Post' + @user_badge.reference_url = Rails.application.routes.path_for(controller: 'posts', action: 'show', + id: @user_badge.badge_source_id) + when 'User' + @user_badge.reference_url = Rails.application.routes.path_for(controller: 'users', action: 'show', + id: @user_badge.badge_source_id) + else + @user_badge.reference_url = '' + end + + if @user_badge.save + content = "You have been awarded a new custom badge '#{@user_badge.badge.title}'!" + link = Rails.application.routes.path_for(controller: 'users', action: 'awarded_badges', + id: @user.id) + @user.create_notification(content, link) + flash[:success] = 'Badge awarded succesfully!' + redirect_to badges_path + else + render :award, status: :bad_request + end + end + private # Use callbacks to share common setup or constraints between actions. @@ -60,8 +103,13 @@ class BadgesController < ApplicationController @badge = Badge.find(params[:id]) end + # Defines the available types of association for the UserBadge polymorphic relationship + def badge_source_types + %w[Post User] + end + # Only allow a list of trusted parameters through. def badge_params - params.require(:badge).permit(:title, :description, :icon) + params.require(:badge).permit(:title, :description, :icon, :manually_awardable, :display_order) end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2528ad8d2876542559f619501bc46e3655e72ade..ba7f6c6677d3a81c8320d42265353a440e07c9fa 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -26,6 +26,20 @@ class UsersController < ApplicationController @post_counts = Post.where(user_id: @users.pluck(:id).uniq).group(:user_id).count end + def search_users + base = if params[:search].present? + user_scope.search(params[:search]) + else + user_scope + end + @users = base.where.not(deleted: true).where.not(community_users: { deleted: true }) + respond_to do |format| + format.json do + render json: @users, only: %i[id username email] + end + end + end + def show @abilities = Ability.on_user(@user) @posts = if current_user&.privilege?('flag_curate') diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb index 67a6dd8e4cf55c81da871f63ac32c98247eeefcb..2ca34c960d1b387a827e20996ae2b9bebf421bf3 100644 --- a/app/models/user_badge.rb +++ b/app/models/user_badge.rb @@ -1,5 +1,6 @@ class UserBadge < ApplicationRecord enum :badge_type, { default: 0, bronze: 1, silver: 2, gold: 3 } + validates :badge_source, presence: true, unless: -> { :manually_awardable } belongs_to :user belongs_to :badge diff --git a/app/views/badges/_form.html.erb b/app/views/badges/_form.html.erb index 6c099743ec00d73d43ed861e99a9017a0a8b6e65..b7abb309403adbc489d6bfab3a07005d54879e67 100644 --- a/app/views/badges/_form.html.erb +++ b/app/views/badges/_form.html.erb @@ -31,5 +31,20 @@ <%= form.text_area :description, class: 'form-element' %> </div> + <div class="form-group"> + <%= form.label :display_order, 'Display order', class: 'form-element' %> + + <span class="form-caption">Each badge must have a display order.</span> + <%= form.number_field :display_order, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= form.check_box :manually_awardable, class: 'form-checkbox-element' %> + <%= form.label :manually_awardable, 'Can be manually awarded?' %> + <span class="form-caption"> + Only check this box if you plan to manually award this badge to users. + </span> + </div> + <%= form.submit 'Save', class: 'button is-filled' %> <% end %> diff --git a/app/views/badges/award.html.erb b/app/views/badges/award.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..b3e2c2e61be17530b02c193e3dec964242e926ab --- /dev/null +++ b/app/views/badges/award.html.erb @@ -0,0 +1,46 @@ +<%= form_for @user_badge, url: false, method: :post do |f| %> + <div class="form-group"> + <%= f.label :user, 'User which should receive the badge', class: 'form-element' %> + <span class="form-caption"> + Select the user who should receive the badge. + </span> + <%= f.select :user, options_for_select([], selected: []), + { include_blank: true }, multiple: false, class: "form-element js-user-select" %> + </div> + + + <div class="form-group"> + <%= f.label :badge, 'Select Badge', class: 'form-element' %> + <span class="form-caption"> + Select which badge should be awarded. In case you want to create a new badge, you can do so + <%= link_to "here", new_badge_path, class: 'link is-underlined', 'aria-label': 'Add new badge' %> + </span> + <%= f.select :badge, options_for_select(Badge.where(:manually_awardable => true).mapping.to_a, selected: []), + { include_blank: false }, multiple: false, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :badge_type, 'Type', class: 'form-element' %> + <%= f.select :badge_type, UserBadge.badge_types.keys, + { include_blank: false }, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :badge_source_type, 'Related Entity', class: 'form-element' %> + + <div class="grid"> + <div class="grid--cell is-6 is-12-sm"> + <div class="form-caption">Entity Type</div> + <%= f.select :badge_source_type, options_for_select(@entity_types, selected: []), + { include_blank: false }, class: 'form-element' %> + </div> + <div class="grid--cell is-6 is-12-sm"> + <div class="form-caption">Id of the entity</div> + <%= f.number_field :badge_source_id, class: 'form-element' %> + </div> + </div> + </div> + + <%= f.submit 'Award Badge', class: 'button is-filled' %> + +<% end %> \ No newline at end of file diff --git a/app/views/badges/index.html.erb b/app/views/badges/index.html.erb index 325724851b4d1ff33a80c913b60f2e99f7a82258..b5fc9ee9511ed7bbb779ce39e90841c1ee1c7377 100644 --- a/app/views/badges/index.html.erb +++ b/app/views/badges/index.html.erb @@ -33,8 +33,8 @@ <div class="notice has-margin-top-4"> <p> You can create new badges. Please bear in mind that for each newly created badge, the functionality to assign the - badge has to be added to the code base, as well. + badge has to be added to the code base, as well. You can also manually award user a badge. </p> <%= link_to "New badge", new_badge_path, class: 'button is-outlined', 'aria-label': 'Add new badge' %> + <%= link_to "Award badge", award_badge_path, class: 'button is-muted is-outlined', 'aria-label': 'Add new badge' %> </div> - diff --git a/config/routes.rb b/config/routes.rb index 89223301a3e6763db34cec713998aea56022823f..c4ee49c1945a4ff2e65e446acfdd7c963bd37983 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,8 @@ Rails.application.routes.draw do get ':id/edit', to: 'badges#edit', as: :edit_badge patch ':id/edit', to: 'badges#update', as: :update_badge delete 'destroy/:id', to: 'badges#destroy', as: :destroy_badge + get 'award', to: 'badges#award', as: :award_badge + post 'award', to: 'badges#award_save', as: :award_badge_save end get 'impersonate/stop', to: 'admin#change_back', as: :stop_impersonating @@ -188,6 +190,7 @@ Rails.application.routes.draw do scope 'users' do root to: 'users#index', as: :users get '/stack-redirect', to: 'users#stack_redirect', as: :stack_redirect + get '/search', to: 'users#search_users', as: :search_users post '/claim-content', to: 'users#transfer_se_content', as: :claim_stack_content get '/mobile-login', to: 'users#qr_login_code', as: :qr_login_code get '/mobile-login/:token', to: 'users#do_qr_login', as: :qr_login diff --git a/db/migrate/20231120160219_add_is_manually_awardable_to_badges.rb b/db/migrate/20231120160219_add_is_manually_awardable_to_badges.rb new file mode 100644 index 0000000000000000000000000000000000000000..c077d4dfe143a051732e6571986a69e288e6dd0c --- /dev/null +++ b/db/migrate/20231120160219_add_is_manually_awardable_to_badges.rb @@ -0,0 +1,5 @@ +class AddIsManuallyAwardableToBadges < ActiveRecord::Migration[7.0] + def change + add_column :badges, :manually_awardable, :boolean, null: false, default: false + end +end diff --git a/db/migrate/20231122173414_make_user_badge_badge_source_not_optional.rb b/db/migrate/20231122173414_make_user_badge_badge_source_not_optional.rb new file mode 100644 index 0000000000000000000000000000000000000000..e0c3703b850e7673bdcc725326de6e604e9c43cf --- /dev/null +++ b/db/migrate/20231122173414_make_user_badge_badge_source_not_optional.rb @@ -0,0 +1,6 @@ +class MakeUserBadgeBadgeSourceNotOptional < ActiveRecord::Migration[7.0] + def change + change_column_null :user_badges, :badge_source_type, true + change_column_null :user_badges, :badge_source_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index 982933e01b9f8de48bb0977b792a8b66c5431b2c..3f0ecfddddb2e7c29e46222f460f92f9047f44e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -100,6 +100,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_11_113515) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "display_order", null: false + t.boolean "manually_awardable", default: false, null: false t.index ["title"], name: "index_badges_on_title", unique: true end @@ -715,8 +716,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_11_113515) do create_table "user_badges", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "badge_id", null: false - t.string "badge_source_type", null: false - t.bigint "badge_source_id", null: false + t.string "badge_source_type" + t.bigint "badge_source_id" t.string "reference_url" t.integer "badge_type", default: 0, null: false t.boolean "pinned", default: false, null: false