From 14cb386f1e9ebb54d6e3e78f377f9108f20d2efc Mon Sep 17 00:00:00 2001 From: Mark VanLandingham Date: Mon, 9 Dec 2019 11:15:47 -0800 Subject: [PATCH] FEATURE: Featured topic for user profile & card (#8461) --- .../components/topic-footer-buttons.js.es6 | 22 ++++- .../components/user-card-contents.js.es6 | 5 + .../discourse/controllers/topic.js.es6 | 17 ++++ .../discourse/controllers/user.js.es6 | 5 + .../initializers/topic-footer-buttons.js.es6 | 29 ++++++ .../javascripts/discourse/models/topic.js.es6 | 15 +++ .../components/user-card-contents.hbs | 23 +++-- .../templates/preferences/profile.hbs | 12 +++ .../javascripts/discourse/templates/topic.hbs | 3 +- .../javascripts/discourse/templates/user.hbs | 8 ++ .../common/components/user-card.scss | 88 +++++++++--------- .../desktop/components/user-card.scss | 18 ++-- .../stylesheets/desktop/topic-post.scss | 3 + .../topic-footer-mobile-dropdown.scss | 5 + .../mobile/components/user-card.scss | 38 ++++---- app/controllers/users_controller.rb | 25 ++++- app/models/topic.rb | 3 + app/models/topic_converter.rb | 1 + app/models/user_profile.rb | 4 + app/serializers/current_user_serializer.rb | 7 +- app/serializers/user_serializer.rb | 6 +- config/locales/client.en.yml | 13 +++ config/locales/server.en.yml | 2 + config/routes.rb | 2 + config/site_settings.yml | 3 + ..._add_featured_topic_id_to_user_profiles.rb | 7 ++ lib/guardian/user_guardian.rb | 7 ++ lib/post_destroyer.rb | 1 + lib/svg_sprite/svg_sprite.rb | 1 + spec/components/post_destroyer_spec.rb | 10 ++ spec/models/topic_converter_spec.rb | 10 ++ spec/models/topic_spec.rb | 13 +++ spec/requests/users_controller_spec.rb | 92 +++++++++++++++++++ .../web_hook_user_serializer_spec.rb | 15 +-- 34 files changed, 418 insertions(+), 95 deletions(-) create mode 100644 db/migrate/20191202202212_add_featured_topic_id_to_user_profiles.rb diff --git a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 index 5f4592aeed3..468734633e9 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 @@ -1,5 +1,6 @@ import discourseComputed from "discourse-common/utils/decorators"; import { alias, or, and } from "@ember/object/computed"; +import { propertyEqual } from "discourse/lib/computed"; import Component from "@ember/component"; import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-button"; @@ -9,6 +10,11 @@ export default Component.extend({ // Allow us to extend it layoutName: "components/topic-footer-buttons", + topicFeaturedOnProfile: propertyEqual( + "topic.id", + "currentUser.featured_topic.id" + ), + @discourseComputed("topic.isPrivateMessage") canArchive(isPM) { return this.siteSettings.enable_personal_messages && isPM; @@ -58,5 +64,19 @@ export default Component.extend({ @discourseComputed("topic.message_archived") archiveLabel: archived => - archived ? "topic.move_to_inbox.title" : "topic.archive_message.title" + archived ? "topic.move_to_inbox.title" : "topic.archive_message.title", + + @discourseComputed( + "topic.user_id", + "topic.isPrivateMessage", + "topic.category.read_restricted" + ) + showToggleFeatureOnProfileButton(userId, isPm, restricted) { + return ( + this.siteSettings.allow_featured_topic_on_user_profiles && + userId === this.currentUser.get("id") && + !restricted && + !isPm + ); + } }); diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index 255e494f21c..6607fe7961e 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -50,6 +50,11 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { // If inside a topic topicPostCount: null, + showFeaturedTopic: and( + "user.featured_topic", + "siteSettings.allow_featured_topic_on_user_profiles" + ), + @discourseComputed("user.staff") staff: isStaff => (isStaff ? "staff" : ""), diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index d273e511534..ed1f8d8add4 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -679,6 +679,19 @@ export default Controller.extend(bufferedProperty("model"), { } }, + toggleFeaturedOnProfile() { + if (!this.currentUser) return; + + if ( + this.currentUser.featured_topic && + this.currentUser.featured_topic.id !== this.model.id + ) { + bootbox.confirm(I18n.t("topic.remove_from_profile.warning"), result => { + if (result) return this._performToggleFeaturedOnProfile(); + }); + } else return this._performToggleFeaturedOnProfile(); + }, + jumpToIndex(index) { this._jumpToIndex(index); }, @@ -1070,6 +1083,10 @@ export default Controller.extend(bufferedProperty("model"), { } }, + _performToggleFeaturedOnProfile() { + this.model.toggleFeaturedOnProfile(this.currentUser).catch(popupAjaxError); + }, + _jumpToIndex(index) { const postStream = this.get("model.postStream"); diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 507ca246904..36bd0c9213f 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -61,6 +61,11 @@ export default Controller.extend(CanCheckEmails, { "hasReceivedWarnings" ), + showFeaturedTopic: and( + "model.featured_topic", + "siteSettings.allow_featured_topic_on_user_profiles" + ), + @discourseComputed("model.suspended", "currentUser.staff") isNotSuspendedOrIsStaff(suspended, isStaff) { return !suspended || isStaff; diff --git a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 index 3cd0827a6f1..4dbca6a9a5e 100644 --- a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 @@ -166,5 +166,34 @@ export default { return this.site.mobileView; } }); + + registerTopicFooterButton({ + dependentKeys: ["currentUser.featured_topic"], + id: "toggle-feature-on-profile", + icon: "id-card", + priority: 300, + label() { + return this.topicFeaturedOnProfile + ? "topic.remove_from_profile.title" + : "topic.feature_on_profile.title"; + }, + title() { + return this.topicFeaturedOnProfile + ? "topic.remove_from_profile.help" + : "topic.feature_on_profile.help"; + }, + classNames() { + return this.topicFeaturedOnProfile + ? ["feature-on-profile", "featured-on-profile"] + : ["feature-on-profile"]; + }, + action: "toggleFeaturedOnProfile", + displayed() { + return this.showToggleFeatureOnProfileButton; + }, + dropdown() { + return this.site.mobileView; + } + }); } }; diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index c392bc20ef6..86515edadcc 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -444,6 +444,21 @@ const Topic = RestModel.extend({ }); }, + toggleFeaturedOnProfile(user) { + const removing = user.get("featured_topic.id") === this.id; + const path = removing ? "clear-featured-topic" : "feature-topic"; + return ajax(`/u/${user.username}/${path}`, { + type: "PUT", + data: { topic_id: this.id } + }) + .then(() => { + const featuredTopic = removing ? null : this; + user.set("featured_topic", featuredTopic); + return; + }) + .catch(popupAjaxError); + }, + createGroupInvite(group) { return ajax(`/t/${this.id}/invite-group`, { type: "POST", diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index a4602fa48e5..4d4cae80996 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -10,13 +10,13 @@
-
+
-
+
-
+
{{else}} @@ -140,8 +140,17 @@
{{/if}} + {{#if showFeaturedTopic}} +
+ +
+ {{/if}} + {{#if hasLocationOrWebsite}} -
+
{{#if user.location}} {{d-icon "map-marker-alt"}} @@ -163,7 +172,7 @@
{{/if}} -
+
{{#unless user.profile_hidden}} {{#if publicUserFields}} -
+
{{#each publicUserFields as |uf|}} {{#if uf.value}} @@ -220,7 +229,7 @@ {{plugin-outlet name="user-card-before-badges" args=(hash user=user)}} {{#if showBadges}} -
+
{{#if user.featured_user_badges}}
{{#each user.featured_user_badges as |ub|}} diff --git a/app/assets/javascripts/discourse/templates/preferences/profile.hbs b/app/assets/javascripts/discourse/templates/preferences/profile.hbs index 7bd63f09302..66ff8457589 100644 --- a/app/assets/javascripts/discourse/templates/preferences/profile.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/profile.hbs @@ -57,6 +57,18 @@
{{/if}} +{{#if model.featured_topic}} +
+ + +
+ {{i18n 'user.change_featured_topic.instructions'}} +
+
+{{/if}} + {{plugin-outlet name="user-preferences-profile" args=(hash model=model save=(action "save"))}} {{plugin-outlet name="user-custom-preferences" args=(hash model=model)}} diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 9d77e4a4b5e..ad6a1557840 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -313,7 +313,8 @@ toggleArchiveMessage=(action "toggleArchiveMessage") editFirstPost=(action "editFirstPost") deferTopic=(action "deferTopic") - replyToPost=(action "replyToPost")}} + replyToPost=(action "replyToPost") + toggleFeaturedOnProfile=(action "toggleFeaturedOnProfile")}} {{else}} + + {{#if showFeaturedTopic}} + + {{/if}} +

{{#if model.location}}{{/if}} {{#if model.website_name}} diff --git a/app/assets/stylesheets/common/components/user-card.scss b/app/assets/stylesheets/common/components/user-card.scss index 21e15ec0b74..506e6e37061 100644 --- a/app/assets/stylesheets/common/components/user-card.scss +++ b/app/assets/stylesheets/common/components/user-card.scss @@ -181,56 +181,62 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards margin-top: 0.5em; } } + // featured topic + .featured-topic { + .desc { + color: $primary-high; + } + a { + color: $primary; + text-decoration: underline; + } + } + // location and website - .third-row { - .location-and-website { + .location-and-website { + display: flex; + flex-wrap: wrap; + width: 100%; + align-items: center; + .location, + .website-name { display: flex; - flex-wrap: wrap; - width: 100%; + overflow: hidden; align-items: center; - .location, - .website-name { - display: flex; - overflow: hidden; - align-items: center; - .d-icon { - margin-right: 0.25em; - } - } - .website-name a, - .location span { - @include ellipsis; - color: $primary; - } - .location { - margin-right: 0.5em; - } - .website-name a { - text-decoration: underline; + .d-icon { + margin-right: 0.25em; } } + .website-name a, + .location span { + @include ellipsis; + color: $primary; + } + .location { + margin-right: 0.5em; + } + .website-name a { + text-decoration: underline; + } } // custom user fields - .fifth-row { - .public-user-fields { - margin: 0; - } + .public-user-fields { + margin: 0; } + // badges - .sixth-row { - .badge-section { - display: flex; - align-items: flex-start; - .user-badge { - @include ellipsis; - background: $primary-very-low; - border: 1px solid $primary-low; - color: $primary; - } - .more-user-badges { - a { - @extend .user-badge; - } + .badge-section { + display: flex; + align-items: flex-start; + .user-badge { + @include ellipsis; + background: $primary-very-low; + border: 1px solid $primary-low; + color: $primary; + } + .more-user-badges { + a { + @extend .user-badge; } } } diff --git a/app/assets/stylesheets/desktop/components/user-card.scss b/app/assets/stylesheets/desktop/components/user-card.scss index 7bc2b0aea1d..f4aa977068f 100644 --- a/app/assets/stylesheets/desktop/components/user-card.scss +++ b/app/assets/stylesheets/desktop/components/user-card.scss @@ -37,16 +37,14 @@ // styles for user cards only #user-card { // badges - .sixth-row { - .badge-section { - .user-badge { - display: block; - max-width: 185px; - margin: 0 0.5em 0 0; - } - .more-user-badges { - max-width: 125px; - } + .badge-section { + .user-badge { + display: block; + max-width: 185px; + margin: 0 0.5em 0 0; + } + .more-user-badges { + max-width: 125px; } } } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 4abaa8d21ac..ee169e499f9 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -451,6 +451,9 @@ nav.post-controls { .bookmark.bookmarked .d-icon-bookmark { color: $tertiary; } + .feature-on-profile.featured-on-profile .d-icon-id-card { + color: $tertiary; + } } #topic-footer-button { diff --git a/app/assets/stylesheets/mobile/components/topic-footer-mobile-dropdown.scss b/app/assets/stylesheets/mobile/components/topic-footer-mobile-dropdown.scss index c5403eaff44..afe761300a4 100644 --- a/app/assets/stylesheets/mobile/components/topic-footer-mobile-dropdown.scss +++ b/app/assets/stylesheets/mobile/components/topic-footer-mobile-dropdown.scss @@ -5,5 +5,10 @@ color: $tertiary; } } + &.featured-on-profile { + .d-icon { + color: $tertiary; + } + } } } diff --git a/app/assets/stylesheets/mobile/components/user-card.scss b/app/assets/stylesheets/mobile/components/user-card.scss index 0345aaf7433..728f93a6475 100644 --- a/app/assets/stylesheets/mobile/components/user-card.scss +++ b/app/assets/stylesheets/mobile/components/user-card.scss @@ -50,30 +50,28 @@ $avatar_width: 120px; // styles for user cards only #user-card { // badges - .sixth-row { - .badge-section { - flex-wrap: wrap; - > span { - display: flex; - flex: 0 1 50%; - max-width: 50%; // for text ellipsis - padding: 2px 0; - box-sizing: border-box; - &:nth-of-type(1), - &:nth-of-type(3) { - padding-right: 4px; - } - a { - width: 100%; - display: flex; - } + .badge-section { + flex-wrap: wrap; + > span { + display: flex; + flex: 0 1 50%; + max-width: 50%; // for text ellipsis + padding: 2px 0; + box-sizing: border-box; + &:nth-of-type(1), + &:nth-of-type(3) { + padding-right: 4px; } - .user-badge { - display: flex; - margin: 0; + a { width: 100%; + display: flex; } } + .user-badge { + display: flex; + margin: 0; + width: 100%; + } } } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3c2fe8382d4..da3a23dfe80 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -11,13 +11,14 @@ class UsersController < ApplicationController :enable_second_factor_totp, :disable_second_factor, :list_second_factors, :update_second_factor, :create_second_factor_backup, :select_avatar, :notification_level, :revoke_auth_token, :register_second_factor_security_key, - :create_second_factor_security_key + :create_second_factor_security_key, :feature_topic, :clear_featured_topic ] skip_before_action :check_xhr, only: [ :show, :badges, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :user_preferences_redirect, :avatar, - :my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login, :summary + :my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login, :summary, + :feature_topic, :clear_featured_topic ] before_action :second_factor_check_confirmed_password, only: [ @@ -1403,6 +1404,22 @@ class UsersController < ApplicationController render json: success_json end + def feature_topic + user = fetch_user_from_params + topic = Topic.find(params[:topic_id].to_i) + + raise Discourse::InvalidAccess.new unless topic && guardian.can_feature_topic?(user, topic) + user.user_profile.update(featured_topic_id: topic.id) + render json: success_json + end + + def clear_featured_topic + user = fetch_user_from_params + guardian.ensure_can_edit!(user) + user.user_profile.update(featured_topic_id: nil) + render json: success_json + end + HONEYPOT_KEY ||= 'HONEYPOT_KEY' CHALLENGE_KEY ||= 'CHALLENGE_KEY' @@ -1457,7 +1474,8 @@ class UsersController < ApplicationController :dismissed_banner_key, :profile_background_upload_url, :card_background_upload_url, - :primary_group_id + :primary_group_id, + :featured_topic_id ] editable_custom_fields = User.editable_user_custom_fields(by_staff: current_user.try(:staff?)) @@ -1532,4 +1550,5 @@ class UsersController < ApplicationController challenge: secure_session["staged-webauthn-challenge-#{user.id}"] } end + end diff --git a/app/models/topic.rb b/app/models/topic.rb index 6e10a13c2f4..113d2fe5c31 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -127,6 +127,7 @@ class Topic < ActiveRecord::Base has_many :invites, through: :topic_invites, source: :invite has_many :topic_timers, dependent: :destroy has_many :reviewables + has_many :user_profiles has_one :user_warning has_one :first_post, -> { where post_number: 1 }, class_name: 'Post' @@ -238,6 +239,8 @@ class Topic < ActiveRecord::Base after_update do if saved_changes[:category_id] && self.tags.present? CategoryTagStat.topic_moved(self, *saved_changes[:category_id]) + elsif saved_changes[:category_id] && self.category&.read_restricted? + UserProfile.remove_featured_topic_from_all_profiles(self) end end diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index 120920b4124..ac353cb31d1 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -50,6 +50,7 @@ class TopicConverter add_allowed_users update_post_uploads_secure_status + UserProfile.remove_featured_topic_from_all_profiles(@topic) Jobs.enqueue(:topic_action_converter, topic_id: @topic.id) Jobs.enqueue(:delete_inaccessible_notifications, topic_id: @topic.id) diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index abd5ba06c25..14f94b4cd01 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -10,6 +10,7 @@ class UserProfile < ActiveRecord::Base belongs_to :card_background_upload, class_name: "Upload" belongs_to :profile_background_upload, class_name: "Upload" belongs_to :granted_title_badge, class_name: "Badge" + belongs_to :featured_topic, class_name: 'Topic' validates :bio_raw, length: { maximum: 3000 } validates :website, url: true, allow_blank: true, if: Proc.new { |c| c.new_record? || c.website_changed? } @@ -145,6 +146,9 @@ class UserProfile < ActiveRecord::Base self.errors.add :base, (I18n.t('user.website.domain_not_allowed', domains: allowed_domains.split('|').join(", "))) unless allowed_domains.split('|').include?(domain) end + def self.remove_featured_topic_from_all_profiles(topic) + where(featured_topic_id: topic.id).update_all(featured_topic_id: nil) + end end # == Schema Information diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index a119bb324a8..af6fe8c5722 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -45,7 +45,8 @@ class CurrentUserSerializer < BasicUserSerializer :second_factor_enabled, :ignored_users, :title_count_mode, - :timezone + :timezone, + :featured_topic def groups object.visible_groups.pluck(:id, :name).map { |id, name| { id: id, name: name.downcase } } @@ -217,4 +218,8 @@ class CurrentUserSerializer < BasicUserSerializer def second_factor_enabled object.totp_enabled? || object.security_keys_enabled? end + + def featured_topic + object.user_profile.featured_topic + end end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 79111200eff..cb7bc9971e4 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -83,7 +83,8 @@ class UserSerializer < BasicUserSerializer :second_factor_remaining_backup_codes, :associated_accounts, :profile_background_upload_url, - :card_background_upload_url + :card_background_upload_url, + :featured_topic has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer @@ -484,4 +485,7 @@ class UserSerializer < BasicUserSerializer object.card_background_upload&.url end + def featured_topic + object.user_profile.featured_topic + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2a2df4d9606..148d47de8ee 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -848,6 +848,7 @@ en: enable_quoting: "Enable quote reply for highlighted text" enable_defer: "Enable defer to mark topics unread" change: "change" + featured_topic: "Featured Topic" moderator: "{{user}} is a moderator" admin: "{{user}} is an admin" moderator_tooltip: "This user is a moderator" @@ -1045,6 +1046,10 @@ en: title: "User Card Background" instructions: "Background images will be centered and have a default width of 590px." + change_featured_topic: + title: "Featured Topic" + instructions: "To change this, either navigate to the topic to remove it as the featured topic, or feature a different topic." + email: title: "Email" primary: "Primary Email" @@ -1997,6 +2002,14 @@ en: defer: help: "Mark as unread" title: "Defer" + feature_on_profile: + help: "Add a link to this topic on your user card and profile" + title: "Feature On Profile" + remove_from_profile: + warning: "Your profile already has a featured topic. If you continue, this topic will replace the existing topic." + help: "Remove the link to this topic on your user profile" + title: "Remove From Profile" + list: "Topics" new: "new topic" unread: "unread" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 8792b061e29..74d947869d1 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1951,6 +1951,8 @@ en: hide_user_profiles_from_public: "Disable user cards, user profiles and user directory for anonymous users." + allow_featured_topic_on_user_profiles: "Allow users to feature a link to a topic on their user card and profile." + show_inactive_accounts: "Allow logged in users to browse profiles of inactive accounts." hide_suspension_reasons: "Don't display suspension reasons publically on user profiles." diff --git a/config/routes.rb b/config/routes.rb index 0c32e16ec86..0edafdffa75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -476,6 +476,8 @@ Discourse::Application.routes.draw do get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username } get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username } get "#{root_path}/:username/profile-hidden" => "users#profile_hidden" + put "#{root_path}/:username/feature-topic" => "users#feature_topic", constraints: { username: RouteFormat.username } + put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic", constraints: { username: RouteFormat.username } end get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json } diff --git a/config/site_settings.yml b/config/site_settings.yml index 698bad76119..53a864d27a1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -560,6 +560,9 @@ users: hide_user_profiles_from_public: default: false client: true + allow_featured_topic_on_user_profiles: + default: true + client: true show_inactive_accounts: default: false user_website_domains_whitelist: diff --git a/db/migrate/20191202202212_add_featured_topic_id_to_user_profiles.rb b/db/migrate/20191202202212_add_featured_topic_id_to_user_profiles.rb new file mode 100644 index 00000000000..7f7e3e75eac --- /dev/null +++ b/db/migrate/20191202202212_add_featured_topic_id_to_user_profiles.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFeaturedTopicIdToUserProfiles < ActiveRecord::Migration[6.0] + def change + add_column :user_profiles, :featured_topic_id, :integer + end +end diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 984969b1f22..667888e29c0 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -123,4 +123,11 @@ module UserGuardian end end end + + def can_feature_topic?(user, topic) + return false if !SiteSetting.allow_featured_topic_on_user_profiles? + return false if !is_me?(user) && !is_staff? + return false if topic.read_restricted_category? || topic.private_message? + topic.user_id === user.id + end end diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 4e09ed8f883..214cb2bbaa1 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -77,6 +77,7 @@ class PostDestroyer WebHook.enqueue_post_hooks(:post_destroyed, @post, payload) if is_first_post + UserProfile.remove_featured_topic_from_all_profiles(@topic) UserActionManager.topic_destroyed(topic) DiscourseEvent.trigger(:topic_destroyed, topic, @user) WebHook.enqueue_topic_hooks(:topic_destroyed, topic, topic_payload) diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index da8ddf72d5f..897c450b002 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -125,6 +125,7 @@ module SvgSprite "heading", "heart", "home", + "id-card", "info-circle", "italic", "key", diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 52c7a18b6b1..6de14c80d48 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -822,4 +822,14 @@ describe PostDestroyer do expect(@reviewable_reply.reload.status).to eq Reviewable.statuses[:approved] end end + + describe "featured topics for user_profiles" do + fab!(:user) { Fabricate(:user) } + + it 'clears the user_profiles featured_topic column' do + user.user_profile.update(featured_topic: post.topic) + PostDestroyer.new(admin, post).destroy + expect(user.user_profile.reload.featured_topic).to eq(nil) + end + end end diff --git a/spec/models/topic_converter_spec.rb b/spec/models/topic_converter_spec.rb index a697b260d48..b439bf6c805 100644 --- a/spec/models/topic_converter_spec.rb +++ b/spec/models/topic_converter_spec.rb @@ -221,5 +221,15 @@ describe TopicConverter do expect(topic.reload.archetype).to eq("private_message") end end + + context 'user_profiles with newly converted PM as featured topic' do + it "sets all matching user_profile featured topic ids to nil" do + author.user_profile.update(featured_topic: topic) + topic.convert_to_private_message(admin) + + expect(author.user_profile.reload.featured_topic).to eq(nil) + end + end + end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 26e2563cd88..6b1d7a070cb 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -2503,4 +2503,17 @@ describe Topic do expect(topic.access_topic_via_group).to eq(open_group) end end + + describe "#after_update" do + fab!(:topic) { Fabricate(:topic, user: user) } + fab!(:category) { Fabricate(:category_with_definition, read_restricted: true) } + + it "removes the topic as featured from user profiles if new category is read_restricted" do + user.user_profile.update(featured_topic: topic) + expect(user.user_profile.featured_topic).to eq(topic) + + topic.update(category: category) + expect(user.user_profile.reload.featured_topic).to eq(nil) + end + end end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index c0f54163f80..e33e526cae2 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -3831,4 +3831,96 @@ describe UsersController do end end end + + describe '#feature_topic' do + fab!(:topic) { Fabricate(:topic) } + fab!(:other_user) { Fabricate(:user) } + fab!(:private_message) { Fabricate(:private_message_topic, user: other_user) } + fab!(:category) { Fabricate(:category_with_definition) } + + describe "site setting enabled" do + before do + SiteSetting.allow_featured_topic_on_user_profiles = true + end + + it 'requires the user to be logged in' do + put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(403) + end + + it 'returns an error if the the current user does not have access' do + sign_in(user) + topic.update(user_id: other_user.id) + put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(403) + end + + it 'returns an error if the user did not create the topic' do + sign_in(user) + topic.update(user_id: other_user.id) + put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(403) + end + + it 'returns an error if the topic is a PM' do + sign_in(other_user) + put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: private_message.id } + expect(response.status).to eq(403) + end + + it "returns an error if the topic's category is read_restricted" do + sign_in(user) + category.set_permissions({}) + topic.update(category_id: category.id) + put "/u/#{other_user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(403) + end + + it 'sets the user_profiles featured_topic correctly' do + sign_in(user) + topic.update(user_id: user.id) + put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(200) + expect(user.user_profile.featured_topic).to eq topic + end + + describe "site setting disabled" do + before do + SiteSetting.allow_featured_topic_on_user_profiles = false + end + + it "does not allow setting featured_topic for user_profiles" do + sign_in(user) + topic.update(user_id: user.id) + put "/u/#{user.username}/feature-topic.json", params: { topic_id: topic.id } + expect(response.status).to eq(403) + end + end + end + end + + describe '#clear_featured_topic' do + fab!(:topic) { Fabricate(:topic) } + fab!(:other_user) { Fabricate(:user) } + + it 'requires the user to be logged in' do + put "/u/#{user.username}/clear-featured-topic.json" + expect(response.status).to eq(403) + end + + it 'returns an error if the the current user does not have access' do + sign_in(user) + topic.update(user_id: other_user.id) + put "/u/#{other_user.username}/clear-featured-topic.json" + expect(response.status).to eq(403) + end + + it 'clears the user_profiles featured_topic correctly' do + sign_in(user) + topic.update(user: user) + put "/u/#{user.username}/clear-featured-topic.json" + expect(response.status).to eq(200) + expect(user.user_profile.featured_topic).to eq nil + end + end end diff --git a/spec/serializers/web_hook_user_serializer_spec.rb b/spec/serializers/web_hook_user_serializer_spec.rb index bdf0f593d65..a019dff350a 100644 --- a/spec/serializers/web_hook_user_serializer_spec.rb +++ b/spec/serializers/web_hook_user_serializer_spec.rb @@ -23,18 +23,13 @@ RSpec.describe WebHookUserSerializer do it 'should only include the required keys' do count = serializer.as_json.keys.count - difference = count - 44 + difference = count - 45 expect(difference).to eq(0), lambda { - message = "" - - if difference < 0 - message << "#{difference * -1} key(s) have been removed from this serializer." - else - message << "#{difference} key(s) have been added to this serializer." - end - - message << "\nPlease verify if those key(s) are required as part of the web hook's payload." + message = (difference < 0 ? + "#{difference * -1} key(s) have been removed from this serializer." : + "#{difference} key(s) have been added to this serializer.") + + "\nPlease verify if those key(s) are required as part of the web hook's payload." } end end