FEATURE: Allow setting avatar flair for automatic groups (#12586)

This commit is contained in:
Neil Lalonde 2021-04-06 11:13:06 -04:00 committed by GitHub
parent 0052fcf7c4
commit e8a9917db1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 389 additions and 29 deletions

View File

@ -0,0 +1,38 @@
import MountWidget from "discourse/components/mount-widget";
import { observes } from "discourse-common/utils/decorators";
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
export default MountWidget.extend({
widget: "avatar-flair",
@observes("user")
_rerender() {
this.queueRerender();
},
buildArgs() {
if (!this.user) {
return;
}
if (this.user.primary_group_flair_url) {
return {
primary_group_flair_url: this.user.primary_group_flair_url,
primary_group_flair_bg_color: this.user.primary_group_flair_bg_color,
primary_group_flair_color: this.user.primary_group_flair_color,
primary_group_name: this.user.primary_group_name,
};
} else {
const autoFlairAttrs = autoGroupFlairForUser(this.site, this.user);
if (autoFlairAttrs) {
return {
primary_group_flair_url: autoFlairAttrs.primary_group_flair_url,
primary_group_flair_bg_color:
autoFlairAttrs.primary_group_flair_bg_color,
primary_group_flair_color: autoFlairAttrs.primary_group_flair_color,
primary_group_name: autoFlairAttrs.primary_group_name,
};
}
}
},
});

View File

@ -0,0 +1,66 @@
let _autoGroupFlair, _noAutoFlair;
export default function autoGroupFlairForUser(site, user) {
if (!_autoGroupFlair) {
initializeAutoGroupFlair(site);
}
if (_noAutoFlair) {
// No automatic groups have flair.
return null;
}
if (user.admin && _autoGroupFlair.admins) {
return _autoGroupFlair.admins;
}
if (user.moderator && _autoGroupFlair.moderators) {
return _autoGroupFlair.moderators;
}
if (_autoGroupFlair.staff && (user.admin || user.moderator)) {
return _autoGroupFlair.staff;
}
let trustLevel = user.trust_level || user.trustLevel;
if (trustLevel) {
for (let i = trustLevel; i >= 0; i--) {
if (_autoGroupFlair[`trust_level_${i}`]) {
return _autoGroupFlair[`trust_level_${i}`];
}
}
}
}
export function resetFlair() {
_autoGroupFlair = null;
_noAutoFlair = null;
}
function initializeAutoGroupFlair(site) {
_autoGroupFlair = {};
_noAutoFlair = true;
[
"admins",
"moderators",
"staff",
"trust_level_0",
"trust_level_1",
"trust_level_2",
"trust_level_3",
"trust_level_4",
].forEach((groupName) => {
const group = site.groups.findBy("name", groupName);
if (group && group.flair_url) {
_noAutoFlair = false;
_autoGroupFlair[groupName] = {
primary_group_flair_url: group.flair_url,
primary_group_flair_bg_color: group.flair_bg_color,
primary_group_flair_color: group.flair_color,
primary_group_name: group.name.replace(/_/g, " "),
};
}
});
}

View File

@ -81,6 +81,7 @@ export function transformBasicPost(post) {
userCustomFields: post.user_custom_fields,
readCount: post.readers_count,
canPublishPage: false,
trustLevel: post.trust_level,
};
_additionalAttributes.forEach((a) => (postAtts[a] = post[a]));

View File

@ -26,6 +26,12 @@
{{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}}
</div>
{{#if model.automatic}}
<div class="control-group">
{{group-flair-inputs model=model}}
</div>
{{/if}}
{{#if canEdit}}
{{yield}}

View File

@ -2,6 +2,7 @@
{{#user-link user=topic.lastPosterUser}}
{{avatar topic.lastPosterUser imageSize="large"}}
{{/user-link}}
{{user-avatar-flair user=topic.lastPosterUser}}
{{#if topic.lastPosterGroup}}
{{avatar-flair
flairURL=topic.lastPosterGroup.flair_url

View File

@ -27,13 +27,9 @@
{{else}}
<a href={{this.user.path}} {{action "showUser" this.user}} class="card-huge-avatar">{{bound-avatar this.user "huge"}}</a>
{{/if}}
{{#if this.user.primary_group_name}}
{{avatar-flair
flairURL=this.user.primary_group_flair_url
flairBgColor=this.user.primary_group_flair_bg_color
flairColor=this.user.primary_group_flair_color
groupName=this.user.primary_group_name}}
{{/if}}
{{user-avatar-flair user=this.user}}
{{plugin-outlet name="user-card-avatar-flair" args=(hash user=this.user) tagName="div"}}
</div>
<div class="names">

View File

@ -1,13 +1,7 @@
<div class="user-image">
<div class="user-image-inner">
<a href={{this.userPath}} data-user-card={{@user.username}}>{{avatar @user imageSize="large"}}</a>
{{#if @user.primary_group_name}}
{{avatar-flair
flairURL=@user.primary_group_flair_url
flairBgColor=@user.primary_group_flair_bg_color
flairColor=@user.primary_group_flair_color
groupName=@user.primary_group_name}}
{{/if}}
{{user-avatar-flair user=@user}}
</div>
</div>

View File

@ -1,11 +1,5 @@
<div class="user-profile-avatar">
{{bound-avatar @user "huge"}}
{{#if @user.primary_group_name}}
{{avatar-flair
flairURL=@user.primary_group_flair_url
flairBgColor=@user.primary_group_flair_bg_color
flairColor=@user.primary_group_flair_color
groupName=@user.primary_group_name}}
{{/if}}
{{user-avatar-flair user=@user}}
{{plugin-outlet name="user-profile-avatar-flair" args=(hash model=@user) tagName="div"}}
</div>

View File

@ -20,6 +20,7 @@ import { postTransformCallbacks } from "discourse/widgets/post-stream";
import { prioritizeNameInUx } from "discourse/lib/settings";
import { relativeAgeMediumSpan } from "discourse/lib/formatter";
import { transformBasicPost } from "discourse/lib/transform-post";
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
function transformWithCallbacks(post) {
let transformed = transformBasicPost(post);
@ -187,6 +188,11 @@ createWidget("post-avatar", {
if (attrs.primary_group_flair_url || attrs.primary_group_flair_bg_color) {
result.push(this.attach("avatar-flair", attrs));
} else {
const autoFlairAttrs = autoGroupFlairForUser(this.site, attrs);
if (autoFlairAttrs) {
result.push(this.attach("avatar-flair", autoFlairAttrs));
}
}
result.push(h("div.poster-avatar-extra"));

View File

@ -4,6 +4,7 @@ import I18n from "I18n";
import { createWidget } from "discourse/widgets/widget";
import { h } from "virtual-dom";
import { replaceEmoji } from "discourse/widgets/emoji";
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
const LINKS_SHOWN = 5;
@ -61,6 +62,11 @@ createWidget("topic-participant", {
if (attrs.primary_group_flair_url || attrs.primary_group_flair_bg_color) {
linkContents.push(this.attach("avatar-flair", attrs));
} else {
const autoFlairAttrs = autoGroupFlairForUser(this.site, attrs);
if (autoFlairAttrs) {
linkContents.push(this.attach("avatar-flair", autoFlairAttrs));
}
}
return h(

View File

@ -0,0 +1,184 @@
import componentTest, {
setupRenderingTest,
} from "discourse/tests/helpers/component-test";
import {
discourseModule,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { resetFlair } from "discourse/lib/avatar-flair";
function setupSiteGroups(that) {
that.site.groups = [
{
id: 1,
name: "admins",
flair_url: "fa-bars",
flair_bg_color: "CC000A",
flair_color: "FFFFFA",
},
{
id: 2,
name: "staff",
flair_url: "fa-bars",
flair_bg_color: "CC0005",
flair_color: "FFFFF5",
},
{
id: 3,
name: "trust_level_1",
flair_url: "fa-dice-one",
flair_bg_color: "CC0001",
flair_color: "FFFFF1",
},
{
id: 4,
name: "trust_level_2",
flair_url: "fa-dice-two",
flair_bg_color: "CC0002",
flair_color: "FFFFF2",
},
];
}
discourseModule(
"Integration | Component | user-avatar-flair",
function (hooks) {
setupRenderingTest(hooks);
componentTest("avatar flair for admin user", {
template: hbs`{{user-avatar-flair user=args}}`,
beforeEach() {
resetFlair();
this.set("args", {
admin: true,
moderator: false,
trust_level: 2,
});
setupSiteGroups(this);
},
afterEach() {
resetFlair();
},
test(assert) {
assert.ok(queryAll(".avatar-flair").length, "it has the tag");
assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon");
assert.equal(
queryAll(".avatar-flair").attr("style"),
"background-color: #CC000A; color: #FFFFFA; ",
"it has styles"
);
},
});
componentTest("avatar flair for moderator user with fallback to staff", {
template: hbs`{{user-avatar-flair user=args}}`,
beforeEach() {
resetFlair();
this.set("args", {
admin: false,
moderator: true,
trust_level: 2,
});
setupSiteGroups(this);
},
afterEach() {
resetFlair();
},
test(assert) {
assert.ok(queryAll(".avatar-flair").length, "it has the tag");
assert.ok(queryAll("svg.d-icon-bars").length, "it has the svg icon");
assert.equal(
queryAll(".avatar-flair").attr("style"),
"background-color: #CC0005; color: #FFFFF5; ",
"it has styles"
);
},
});
componentTest("avatar flair for trust level", {
template: hbs`{{user-avatar-flair user=args}}`,
beforeEach() {
resetFlair();
this.set("args", {
admin: false,
moderator: false,
trust_level: 2,
});
setupSiteGroups(this);
},
afterEach() {
resetFlair();
},
test(assert) {
assert.ok(queryAll(".avatar-flair").length, "it has the tag");
assert.ok(
queryAll("svg.d-icon-dice-two").length,
"it has the svg icon"
);
assert.equal(
queryAll(".avatar-flair").attr("style"),
"background-color: #CC0002; color: #FFFFF2; ",
"it has styles"
);
},
});
componentTest("avatar flair for trust level with fallback", {
template: hbs`{{user-avatar-flair user=args}}`,
beforeEach() {
resetFlair();
this.set("args", {
admin: false,
moderator: false,
trust_level: 3,
});
setupSiteGroups(this);
},
afterEach() {
resetFlair();
},
test(assert) {
assert.ok(queryAll(".avatar-flair").length, "it has the tag");
assert.ok(
queryAll("svg.d-icon-dice-two").length,
"it has the svg icon"
);
assert.equal(
queryAll(".avatar-flair").attr("style"),
"background-color: #CC0002; color: #FFFFF2; ",
"it has styles"
);
},
});
componentTest("avatar flair for primary group flair", {
template: hbs`{{user-avatar-flair user=args}}`,
beforeEach() {
resetFlair();
this.set("args", {
admin: false,
moderator: false,
trust_level: 3,
primary_group_flair_url: "fa-times",
primary_group_flair_bg_color: "123456",
primary_group_flair_color: "B0B0B0",
primary_group_name: "Band Geeks",
});
setupSiteGroups(this);
},
afterEach() {
resetFlair();
},
test(assert) {
assert.ok(queryAll(".avatar-flair").length, "it has the tag");
assert.ok(queryAll("svg.d-icon-times").length, "it has the svg icon");
assert.equal(
queryAll(".avatar-flair").attr("style"),
"background-color: #123456; color: #B0B0B0; ",
"it has styles"
);
},
});
}
);

View File

@ -579,6 +579,10 @@ class GroupsController < ApplicationController
messageable_level
default_notification_level
bio_raw
flair_icon
flair_upload_id
flair_bg_color
flair_color
}
else
default_params = %i{

View File

@ -3,7 +3,7 @@
class CategoryAndTopicListsSerializer < ApplicationSerializer
has_one :category_list, serializer: CategoryListSerializer, embed: :objects
has_one :topic_list, serializer: TopicListSerializer, embed: :objects
has_many :users, serializer: BasicUserSerializer, embed: :objects
has_many :users, serializer: PosterSerializer, embed: :objects
has_many :primary_groups, serializer: PrimaryGroupSerializer, embed: :objects
def users

View File

@ -6,7 +6,10 @@ module UserPrimaryGroupMixin
klass.attributes :primary_group_name,
:primary_group_flair_url,
:primary_group_flair_bg_color,
:primary_group_flair_color
:primary_group_flair_color,
:admin,
:moderator,
:trust_level
end
def primary_group_name
@ -41,4 +44,19 @@ module UserPrimaryGroupMixin
object&.primary_group&.flair_color.present?
end
def include_admin?
object&.admin
end
def admin
true
end
def include_moderator?
object&.moderator
end
def moderator
true
end
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class PosterSerializer < BasicUserSerializer
include UserPrimaryGroupMixin
end

View File

@ -62,7 +62,17 @@ class SiteSerializer < ApplicationSerializer
def groups
cache_anon_fragment("group_names") do
object.groups.order(:name).pluck(:id, :name).map { |id, name| { id: id, name: name } }.as_json
object.groups.order(:name)
.select(:id, :name, :flair_icon, :flair_upload_id, :flair_bg_color, :flair_color)
.map do |g|
{
id: g.id,
name: g.name,
flair_url: g.flair_url,
flair_bg_color: g.flair_bg_color,
flair_color: g.flair_color,
}
end.as_json
end
end

View File

@ -3,7 +3,8 @@
class TopicPostCountSerializer < BasicUserSerializer
attributes :post_count, :primary_group_name,
:primary_group_flair_url, :primary_group_flair_color, :primary_group_flair_bg_color
:primary_group_flair_url, :primary_group_flair_color, :primary_group_flair_bg_color,
:admin, :moderator, :trust_level,
def id
object[:user].id
@ -34,4 +35,24 @@ class TopicPostCountSerializer < BasicUserSerializer
object[:user]&.primary_group&.flair_color
end
def include_admin?
object[:user].admin
end
def admin
true
end
def include_moderator?
object[:user].moderator
end
def moderator
true
end
def trust_level
object[:user].trust_level
end
end

View File

@ -2,6 +2,7 @@
class TopicPosterSerializer < ApplicationSerializer
attributes :extras, :description
has_one :user, serializer: BasicUserSerializer
has_one :user, serializer: PosterSerializer
has_one :primary_group, serializer: PrimaryGroupSerializer
end

View File

@ -18,7 +18,7 @@ class UserLookup
private
def self.lookup_columns
@user_lookup_columns ||= %i{id username name uploaded_avatar_id primary_group_id}
@user_lookup_columns ||= %i{id username name uploaded_avatar_id primary_group_id admin moderator trust_level}
end
def self.group_lookup_columns

View File

@ -815,7 +815,9 @@ describe GroupsController do
put "/groups/#{group.id}.json", params: {
group: {
flair_bg_color: 'FFF',
flair_color: 'BBB',
flair_icon: 'fa-adjust',
name: 'testing',
visibility_level: 1,
mentionable_level: 1,
@ -829,7 +831,9 @@ describe GroupsController do
expect(response.status).to eq(200)
group.reload
expect(group.flair_color).to eq(nil)
expect(group.flair_bg_color).to eq('FFF')
expect(group.flair_color).to eq('BBB')
expect(group.flair_icon).to eq('fa-adjust')
expect(group.name).to eq('admins')
expect(group.visibility_level).to eq(1)
expect(group.mentionable_level).to eq(1)
@ -916,6 +920,9 @@ describe GroupsController do
put "/groups/#{group.id}.json", params: {
group: {
flair_bg_color: 'FFF',
flair_color: 'BBB',
flair_icon: 'fa-adjust',
mentionable_level: 1,
messageable_level: 1,
default_notification_level: 1
@ -925,7 +932,9 @@ describe GroupsController do
expect(response.status).to eq(200)
group.reload
expect(group.flair_color).to eq(nil)
expect(group.flair_bg_color).to eq('FFF')
expect(group.flair_color).to eq('BBB')
expect(group.flair_icon).to eq('fa-adjust')
expect(group.name).to eq('trust_level_4')
expect(group.mentionable_level).to eq(1)
expect(group.messageable_level).to eq(1)