FEATURE: lets users favorite 2 badges to show on user-card (#13151)

This commit is contained in:
Joffrey JAFFEUX 2021-06-01 10:33:40 +02:00 committed by GitHub
parent c5174e6982
commit 1cd0424ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 187 additions and 11 deletions

View File

@ -31,4 +31,9 @@ export default Component.extend({
}
return sanitize(description);
},
@discourseComputed("badge.id")
showFavorite(badgeId) {
return ![1, 2, 3, 4].includes(badgeId);
},
});

View File

@ -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();
},
});

View File

@ -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;
}
},

View File

@ -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>

View File

@ -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}}

View File

@ -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);

View File

@ -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);
});
});

View File

@ -143,6 +143,12 @@
font-size: $font-up-2;
}
.favorite-btn {
position: absolute;
right: 0;
bottom: 0;
}
.badge-contents {
display: flex;
min-height: 128px;

View File

@ -60,6 +60,10 @@
flex-wrap: wrap;
}
&.user-badges-list .favorite-count {
flex: 100%;
}
.btn.right {
float: right;
}

View File

@ -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

View File

@ -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)
#

View File

@ -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

View File

@ -3617,6 +3617,9 @@ en:
name: Other
posting:
name: Posting
favorite_max_reached: "You cant 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"

View File

@ -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')

View File

@ -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

View File

@ -113,6 +113,7 @@ module SvgSprite
"far-moon",
"far-smile",
"far-square",
"far-star",
"far-sun",
"far-thumbs-down",
"far-thumbs-up",

View File

@ -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