FEATURE: lets users favorite 2 badges to show on user-card (#13151)
This commit is contained in:
parent
c5174e6982
commit
1cd0424ccd
|
@ -31,4 +31,9 @@ export default Component.extend({
|
|||
}
|
||||
return sanitize(description);
|
||||
},
|
||||
|
||||
@discourseComputed("badge.id")
|
||||
showFavorite(badgeId) {
|
||||
return ![1, 2, 3, 4].includes(badgeId);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { alias, sort } from "@ember/object/computed";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { alias, filterBy, sort } from "@ember/object/computed";
|
||||
|
||||
export default Controller.extend({
|
||||
user: controller(),
|
||||
username: alias("user.model.username_lower"),
|
||||
sortedBadges: sort("model", "badgeSortOrder"),
|
||||
favoriteBadges: filterBy("model", "is_favorite", true),
|
||||
canFavoriteMoreBadges: computed(
|
||||
"favoriteBadges.length",
|
||||
"model.meta.max_favorites",
|
||||
function () {
|
||||
return this.favoriteBadges.length < this.model.meta.max_favorites;
|
||||
}
|
||||
),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.badgeSortOrder = ["badge.badge_type.sort_order:desc", "badge.name"];
|
||||
},
|
||||
|
||||
@action
|
||||
favorite(badge) {
|
||||
return badge.favorite();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,8 +4,11 @@ import { Promise } from "rsvp";
|
|||
import Topic from "discourse/models/topic";
|
||||
import User from "discourse/models/user";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
const DEFAULT_USER_BADGES_META = { max_favorites: 2 };
|
||||
|
||||
const UserBadge = EmberObject.extend({
|
||||
@discourseComputed
|
||||
postUrl: function () {
|
||||
|
@ -19,6 +22,15 @@ const UserBadge = EmberObject.extend({
|
|||
type: "DELETE",
|
||||
});
|
||||
},
|
||||
|
||||
favorite() {
|
||||
return ajax(`/user_badges/${this.id}/toggle_favorite`, { type: "PUT" })
|
||||
.then((json) => {
|
||||
this.set("is_favorite", json.user_badge.is_favorite);
|
||||
return this;
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
});
|
||||
|
||||
UserBadge.reopenClass({
|
||||
|
@ -86,6 +98,7 @@ UserBadge.reopenClass({
|
|||
userBadges.grant_count = json.user_badge_info.grant_count;
|
||||
userBadges.username = json.user_badge_info.username;
|
||||
}
|
||||
userBadges.meta = json.meta || DEFAULT_USER_BADGES_META;
|
||||
return userBadges;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,7 +4,26 @@
|
|||
{{#if badge.has_badge}}
|
||||
<a href={{url}} class="check-display status-checked">{{d-icon "check"}}</a>
|
||||
{{/if}}
|
||||
<div class="badge-contents">
|
||||
|
||||
{{#if canFavorite}}
|
||||
{{#if isFavorite}}
|
||||
{{d-button
|
||||
icon="star"
|
||||
class="favorite-btn"
|
||||
action=onFavoriteClick
|
||||
}}
|
||||
{{else}}
|
||||
{{d-button
|
||||
icon="far-star"
|
||||
class="favorite-btn"
|
||||
action=onFavoriteClick
|
||||
title=(if canFavoriteMoreBadges "badges.favorite_max_not_reached" "badges.favorite_max_reached")
|
||||
disabled=(not canFavoriteMoreBadges)
|
||||
}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="badge-contents" >
|
||||
<div class="badge-icon {{badge.badgeTypeClassName}}">
|
||||
<a href={{url}}>{{icon-or-image badge}}</a>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
{{#d-section pageClass="user-badges" class="user-content user-badges-list"}}
|
||||
<p class="favorite-count">
|
||||
{{i18n "badges.favorite_count" count=this.favoriteBadges.length max=model.meta.max_favorites}}
|
||||
</p>
|
||||
{{#each sortedBadges as |ub|}}
|
||||
{{badge-card
|
||||
badge=ub.badge
|
||||
count=ub.count
|
||||
canFavorite=ub.can_favorite
|
||||
isFavorite=ub.is_favorite
|
||||
username=username
|
||||
canFavoriteMoreBadges=canFavoriteMoreBadges
|
||||
onFavoriteClick=(action "favorite" ub)
|
||||
filterUser="true"}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
|
|
|
@ -576,6 +576,9 @@ export function applyDefaultHandlers(pretender) {
|
|||
response(200, fixturesByUrl["/user_badges"])
|
||||
);
|
||||
pretender.delete("/user_badges/:badge_id", success);
|
||||
pretender.put("/user_badges/:id/toggle_favorite", () =>
|
||||
response(200, { user_badge: { is_favorite: true } })
|
||||
);
|
||||
|
||||
pretender.post("/posts", function (request) {
|
||||
const data = parsePostData(request.requestBody);
|
||||
|
|
|
@ -57,4 +57,12 @@ module("Unit | Model | user-badge", function () {
|
|||
const userBadge = UserBadge.create({ id: 1 });
|
||||
await userBadge.revoke();
|
||||
});
|
||||
|
||||
test("favorite", async function (assert) {
|
||||
const userBadge = UserBadge.create({ id: 1 });
|
||||
assert.notOk(userBadge.is_favorite);
|
||||
|
||||
await userBadge.favorite();
|
||||
assert.ok(userBadge.is_favorite);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -143,6 +143,12 @@
|
|||
font-size: $font-up-2;
|
||||
}
|
||||
|
||||
.favorite-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.badge-contents {
|
||||
display: flex;
|
||||
min-height: 128px;
|
||||
|
|
|
@ -60,6 +60,10 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.user-badges-list .favorite-count {
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
.btn.right {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserBadgesController < ApplicationController
|
||||
MAX_FAVORITES = 2
|
||||
MAX_BADGES = 96 # This was limited in PR#2360 to make it divisible by 8
|
||||
|
||||
before_action :ensure_badges_enabled
|
||||
|
||||
def index
|
||||
params.permit [:granted_before, :offset, :username]
|
||||
|
||||
badge = fetch_badge_from_params
|
||||
user_badges = badge.user_badges.order('granted_at DESC, id DESC').limit(96)
|
||||
user_badges = badge.user_badges.order('granted_at DESC, id DESC').limit(MAX_BADGES)
|
||||
user_badges = user_badges.includes(:user, :granted_by, badge: :badge_type, post: :topic, user: :primary_group)
|
||||
|
||||
grant_count = nil
|
||||
|
@ -37,15 +40,19 @@ class UserBadgesController < ApplicationController
|
|||
user_badges = user.user_badges
|
||||
|
||||
if params[:grouped]
|
||||
user_badges = user_badges.group(:badge_id)
|
||||
.select(UserBadge.attribute_names.map { |x| "MAX(#{x}) AS #{x}" }, 'COUNT(*) AS "count"')
|
||||
user_badges = user_badges.group(:badge_id).select_for_grouping
|
||||
end
|
||||
|
||||
user_badges = user_badges.includes(badge: [:badge_grouping, :badge_type, :image_upload])
|
||||
.includes(post: :topic)
|
||||
.includes(:granted_by)
|
||||
|
||||
render_serialized(user_badges, DetailedUserBadgeSerializer, root: :user_badges)
|
||||
render_serialized(
|
||||
user_badges,
|
||||
DetailedUserBadgeSerializer,
|
||||
root: :user_badges,
|
||||
meta: { max_favorites: MAX_FAVORITES },
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -91,6 +98,24 @@ class UserBadgesController < ApplicationController
|
|||
render json: success_json
|
||||
end
|
||||
|
||||
def toggle_favorite
|
||||
params.require(:user_badge_id)
|
||||
user_badge = UserBadge.find(params[:user_badge_id])
|
||||
user_badges = user_badge.user.user_badges
|
||||
|
||||
unless can_favorite_badge?(user_badge)
|
||||
return render json: failed_json, status: 403
|
||||
end
|
||||
|
||||
if !user_badge.is_favorite && user_badges.where(is_favorite: true).count >= MAX_FAVORITES
|
||||
return render json: failed_json, status: 400
|
||||
end
|
||||
|
||||
user_badge.toggle!(:is_favorite)
|
||||
UserBadge.update_featured_ranks!(user_badge.user_id)
|
||||
render_serialized(user_badge, DetailedUserBadgeSerializer, root: :user_badge)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Get the badge from either the badge name or id specified in the params.
|
||||
|
@ -114,6 +139,10 @@ class UserBadgesController < ApplicationController
|
|||
master_api_call || guardian.can_grant_badges?(user)
|
||||
end
|
||||
|
||||
def can_favorite_badge?(user_badge)
|
||||
current_user == user_badge.user && !(1..4).include?(user_badge.badge_id)
|
||||
end
|
||||
|
||||
def ensure_badges_enabled
|
||||
raise Discourse::NotFound unless SiteSetting.enable_badges?
|
||||
end
|
||||
|
|
|
@ -9,12 +9,24 @@ class UserBadge < ActiveRecord::Base
|
|||
|
||||
scope :grouped_with_count, -> {
|
||||
group(:badge_id, :user_id)
|
||||
.select(UserBadge.attribute_names.map { |x| "MAX(user_badges.#{x}) AS #{x}" },
|
||||
'COUNT(*) AS "count"')
|
||||
.select_for_grouping
|
||||
.order('MAX(featured_rank) ASC')
|
||||
.includes(:user, :granted_by, { badge: :badge_type }, post: :topic)
|
||||
}
|
||||
|
||||
scope :select_for_grouping, -> {
|
||||
select(
|
||||
UserBadge.attribute_names.map do |name|
|
||||
if name == 'is_favorite'
|
||||
"BOOL_OR(user_badges.#{name}) AS is_favorite"
|
||||
else
|
||||
"MAX(user_badges.#{name}) AS #{name}"
|
||||
end
|
||||
end,
|
||||
'COUNT(*) AS "count"'
|
||||
)
|
||||
}
|
||||
|
||||
scope :for_enabled_badges, -> { where('user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)') }
|
||||
|
||||
validates :badge_id,
|
||||
|
@ -62,6 +74,7 @@ class UserBadge < ActiveRecord::Base
|
|||
PARTITION BY user_badges.user_id -- Do a separate rank for each user
|
||||
ORDER BY BOOL_OR(badges.enabled) DESC, -- Disabled badges last
|
||||
MAX(featured_tl_badge.user_id) NULLS LAST, -- Best tl badge first
|
||||
BOOL_OR(user_badges.is_favorite) DESC NULLS LAST, -- Favorite badges next
|
||||
CASE WHEN user_badges.badge_id IN (1,2,3,4) THEN 1 ELSE 0 END ASC, -- Non-featured tl badges last
|
||||
MAX(badges.badge_type_id) ASC,
|
||||
MAX(badges.grant_count) ASC,
|
||||
|
@ -102,6 +115,7 @@ end
|
|||
# seq :integer default(0), not null
|
||||
# featured_rank :integer
|
||||
# created_at :datetime not null
|
||||
# is_favorite :boolean
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
@ -109,4 +123,5 @@ end
|
|||
# index_user_badges_on_badge_id_and_user_id_and_post_id (badge_id,user_id,post_id) UNIQUE WHERE (post_id IS NOT NULL)
|
||||
# index_user_badges_on_badge_id_and_user_id_and_seq (badge_id,user_id,seq) UNIQUE WHERE (post_id IS NULL)
|
||||
# index_user_badges_on_user_id (user_id)
|
||||
# index_user_badges_on_is_favorite (is_favorite)
|
||||
#
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class DetailedUserBadgeSerializer < BasicUserBadgeSerializer
|
||||
has_one :granted_by, serializer: UserBadgeSerializer::UserSerializer
|
||||
|
||||
attributes :post_number, :topic_id, :topic_title
|
||||
attributes :post_number, :topic_id, :topic_title, :is_favorite, :can_favorite
|
||||
|
||||
def include_post_number?
|
||||
object.post
|
||||
|
@ -24,4 +24,8 @@ class DetailedUserBadgeSerializer < BasicUserBadgeSerializer
|
|||
object.post.topic.title if object.post && object.post.topic
|
||||
end
|
||||
|
||||
def can_favorite
|
||||
(scope.current_user.present? && object.user_id == scope.current_user.id) &&
|
||||
!(1..4).include?(object.badge_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3617,6 +3617,9 @@ en:
|
|||
name: Other
|
||||
posting:
|
||||
name: Posting
|
||||
favorite_max_reached: "You can’t favorite more badges."
|
||||
favorite_max_not_reached: "Mark this badge as favorite"
|
||||
favorite_count: "%{count}/%{max} badges marked as favorite"
|
||||
|
||||
tagging:
|
||||
all_tags: "All Tags"
|
||||
|
|
|
@ -671,7 +671,9 @@ Discourse::Application.routes.draw do
|
|||
|
||||
resources :badges, only: [:index]
|
||||
get "/badges/:id(/:slug)" => "badges#show", constraints: { format: /(json|html|rss)/ }
|
||||
resources :user_badges, only: [:index, :create, :destroy]
|
||||
resources :user_badges, only: [:index, :create, :destroy] do
|
||||
put "toggle_favorite" => "user_badges#toggle_favorite", constraints: { format: :json }
|
||||
end
|
||||
|
||||
get '/c', to: redirect(relative_url_root + 'categories')
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIsFavoriteToUserBadge < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :user_badges, :is_favorite, :boolean
|
||||
end
|
||||
end
|
|
@ -113,6 +113,7 @@ module SvgSprite
|
|||
"far-moon",
|
||||
"far-smile",
|
||||
"far-square",
|
||||
"far-star",
|
||||
"far-sun",
|
||||
"far-thumbs-down",
|
||||
"far-thumbs-up",
|
||||
|
|
|
@ -267,4 +267,41 @@ describe UserBadgesController do
|
|||
expect(events).to include(:user_badge_removed)
|
||||
end
|
||||
end
|
||||
|
||||
context "favorite" do
|
||||
let!(:user_badge) { UserBadge.create(badge: badge, user: user, granted_by: Discourse.system_user, granted_at: Time.now) }
|
||||
|
||||
it "checks that the user is authorized to favorite the badge" do
|
||||
sign_in(Fabricate(:admin))
|
||||
put "/user_badges/#{user_badge.id}/toggle_favorite.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "checks that the user has less than two favorited badges" do
|
||||
sign_in(user)
|
||||
UserBadge.create(badge: Fabricate(:badge), user: user, granted_by: Discourse.system_user, granted_at: Time.now, is_favorite: true)
|
||||
UserBadge.create(badge: Fabricate(:badge), user: user, granted_by: Discourse.system_user, granted_at: Time.now, is_favorite: true)
|
||||
put "/user_badges/#{user_badge.id}/toggle_favorite.json"
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it "favorites a badge" do
|
||||
sign_in(user)
|
||||
put "/user_badges/#{user_badge.id}/toggle_favorite.json"
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
user_badge = UserBadge.find_by(user: user, badge: badge)
|
||||
expect(user_badge.is_favorite).to be true
|
||||
end
|
||||
|
||||
it "unfavorites a badge" do
|
||||
sign_in(user)
|
||||
user_badge.toggle!(:is_favorite)
|
||||
put "/user_badges/#{user_badge.id}/toggle_favorite.json"
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
user_badge = UserBadge.find_by(user: user, badge: badge)
|
||||
expect(user_badge.is_favorite).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue