mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
FEATURE: Send notifications to admins when new features are released (#19460)
This commit adds a new notification that gets sent to admins when the site gets new features after an upgrade/deploy. Clicking on the notification takes the admin to the admin dashboard at `/admin` where they can see the new features under the "New Features" section. Internal topic: t/87166.
This commit is contained in:
@ -10,6 +10,7 @@ import LikedConsolidated from "discourse/lib/notification-types/liked-consolidat
|
|||||||
import Liked from "discourse/lib/notification-types/liked";
|
import Liked from "discourse/lib/notification-types/liked";
|
||||||
import MembershipRequestAccepted from "discourse/lib/notification-types/membership-request-accepted";
|
import MembershipRequestAccepted from "discourse/lib/notification-types/membership-request-accepted";
|
||||||
import MembershipRequestConsolidated from "discourse/lib/notification-types/membership-request-consolidated";
|
import MembershipRequestConsolidated from "discourse/lib/notification-types/membership-request-consolidated";
|
||||||
|
import NewFeatures from "discourse/lib/notification-types/new-features";
|
||||||
import MovedPost from "discourse/lib/notification-types/moved-post";
|
import MovedPost from "discourse/lib/notification-types/moved-post";
|
||||||
import WatchingFirstPost from "discourse/lib/notification-types/watching-first-post";
|
import WatchingFirstPost from "discourse/lib/notification-types/watching-first-post";
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ const CLASS_FOR_TYPE = {
|
|||||||
membership_request_accepted: MembershipRequestAccepted,
|
membership_request_accepted: MembershipRequestAccepted,
|
||||||
membership_request_consolidated: MembershipRequestConsolidated,
|
membership_request_consolidated: MembershipRequestConsolidated,
|
||||||
moved_post: MovedPost,
|
moved_post: MovedPost,
|
||||||
|
new_features: NewFeatures,
|
||||||
watching_first_post: WatchingFirstPost,
|
watching_first_post: WatchingFirstPost,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import NotificationTypeBase from "discourse/lib/notification-types/base";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
export default class extends NotificationTypeBase {
|
||||||
|
get label() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get description() {
|
||||||
|
return I18n.t("notifications.new_features");
|
||||||
|
}
|
||||||
|
|
||||||
|
get linkHref() {
|
||||||
|
return getURL("/admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return "gift";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
import { iconNode } from "discourse-common/lib/icon-library";
|
||||||
|
|
||||||
|
createWidgetFrom(DefaultNotificationItem, "new-features-notification-item", {
|
||||||
|
text() {
|
||||||
|
return I18n.t("notifications.new_features");
|
||||||
|
},
|
||||||
|
|
||||||
|
url() {
|
||||||
|
return getURL("/admin");
|
||||||
|
},
|
||||||
|
|
||||||
|
icon() {
|
||||||
|
return iconNode("gift");
|
||||||
|
},
|
||||||
|
});
|
@ -24,8 +24,14 @@ class Admin::DashboardController < Admin::StaffController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new_features
|
def new_features
|
||||||
|
new_features = DiscourseUpdates.new_features
|
||||||
|
|
||||||
|
if current_user.admin? && most_recent = new_features&.first
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent["created_at"])
|
||||||
|
end
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
new_features: DiscourseUpdates.new_features,
|
new_features: new_features,
|
||||||
has_unseen_features: DiscourseUpdates.has_unseen_features?(current_user.id),
|
has_unseen_features: DiscourseUpdates.has_unseen_features?(current_user.id),
|
||||||
release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"]
|
release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"]
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,50 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
|
|
||||||
class CheckNewFeatures < ::Jobs::Scheduled
|
class CheckNewFeatures < ::Jobs::Scheduled
|
||||||
every 1.day
|
every 1.day
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
@new_features_json ||= DiscourseUpdates.new_features_payload
|
admin_ids = User.human_users.where(admin: true).pluck(:id)
|
||||||
DiscourseUpdates.update_new_features(@new_features_json)
|
|
||||||
|
prev_most_recent = DiscourseUpdates.new_features&.first
|
||||||
|
if prev_most_recent
|
||||||
|
admin_ids.each do |admin_id|
|
||||||
|
if DiscourseUpdates.get_last_viewed_feature_date(admin_id).blank?
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(
|
||||||
|
admin_id,
|
||||||
|
prev_most_recent["created_at"]
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# this memoization may seem pointless, but it actually avoids us hitting
|
||||||
|
# Meta repeatedly and getting rate-limited when this job is ran on a
|
||||||
|
# multisite cluster.
|
||||||
|
# in multisite, the `execute` method (of the same instance) is called for
|
||||||
|
# every site in the cluster.
|
||||||
|
@new_features_json ||= DiscourseUpdates.new_features_payload
|
||||||
|
DiscourseUpdates.update_new_features(@new_features_json)
|
||||||
|
|
||||||
|
new_most_recent = DiscourseUpdates.new_features&.first
|
||||||
|
if new_most_recent
|
||||||
|
most_recent_feature_date = Time.zone.parse(new_most_recent["created_at"])
|
||||||
|
admin_ids.each do |admin_id|
|
||||||
|
admin_last_viewed_feature_date = DiscourseUpdates.get_last_viewed_feature_date(admin_id)
|
||||||
|
if admin_last_viewed_feature_date.blank? || admin_last_viewed_feature_date < most_recent_feature_date
|
||||||
|
Notification.consolidate_or_create!(
|
||||||
|
user_id: admin_id,
|
||||||
|
notification_type: Notification.types[:new_features],
|
||||||
|
data: {}
|
||||||
|
)
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(
|
||||||
|
admin_id,
|
||||||
|
new_most_recent["created_at"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -139,6 +139,7 @@ class Notification < ActiveRecord::Base
|
|||||||
assigned: 34,
|
assigned: 34,
|
||||||
question_answer_user_commented: 35, # Used by https://github.com/discourse/discourse-question-answer
|
question_answer_user_commented: 35, # Used by https://github.com/discourse/discourse-question-answer
|
||||||
watching_category_or_tag: 36,
|
watching_category_or_tag: 36,
|
||||||
|
new_features: 37,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ module Notifications
|
|||||||
private
|
private
|
||||||
|
|
||||||
def plan_for(notification)
|
def plan_for(notification)
|
||||||
consolidation_plans = [liked_by_two_users, liked, group_message_summary, group_membership]
|
consolidation_plans = [liked_by_two_users, liked, group_message_summary, group_membership, new_features_notification]
|
||||||
consolidation_plans.concat(DiscoursePluginRegistry.notification_consolidation_plans)
|
consolidation_plans.concat(DiscoursePluginRegistry.notification_consolidation_plans)
|
||||||
|
|
||||||
consolidation_plans.detect { |plan| plan.can_consolidate_data?(notification) }
|
consolidation_plans.detect { |plan| plan.can_consolidate_data?(notification) }
|
||||||
@ -122,5 +122,9 @@ module Notifications
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_features_notification
|
||||||
|
DeletePreviousNotifications.new(type: Notification.types[:new_features])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
module Notifications
|
module Notifications
|
||||||
class DeletePreviousNotifications < ConsolidationPlan
|
class DeletePreviousNotifications < ConsolidationPlan
|
||||||
def initialize(type:, previous_query_blk:)
|
def initialize(type:, previous_query_blk: nil)
|
||||||
@type = type
|
@type = type
|
||||||
@previous_query_blk = previous_query_blk
|
@previous_query_blk = previous_query_blk
|
||||||
end
|
end
|
||||||
|
@ -2539,6 +2539,7 @@ en:
|
|||||||
reaction: "<span>%{username}</span> %{description}"
|
reaction: "<span>%{username}</span> %{description}"
|
||||||
reaction_2: "<span>%{username}, %{username2}</span> %{description}"
|
reaction_2: "<span>%{username}, %{username2}</span> %{description}"
|
||||||
votes_released: "%{description} - completed"
|
votes_released: "%{description} - completed"
|
||||||
|
new_features: "New features available!"
|
||||||
dismiss_confirmation:
|
dismiss_confirmation:
|
||||||
body:
|
body:
|
||||||
default:
|
default:
|
||||||
@ -2596,6 +2597,7 @@ en:
|
|||||||
membership_request_consolidated: "new membership requests"
|
membership_request_consolidated: "new membership requests"
|
||||||
reaction: "new reaction"
|
reaction: "new reaction"
|
||||||
votes_released: "Vote was released"
|
votes_released: "Vote was released"
|
||||||
|
new_features: "new Discourse features have been released!"
|
||||||
|
|
||||||
upload_selector:
|
upload_selector:
|
||||||
uploading: "Uploading"
|
uploading: "Uploading"
|
||||||
|
@ -176,6 +176,16 @@ module DiscourseUpdates
|
|||||||
Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
|
Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_last_viewed_feature_date(user_id)
|
||||||
|
date = Discourse.redis.hget(last_viewed_feature_dates_for_users_key, user_id.to_s)
|
||||||
|
return if date.blank?
|
||||||
|
Time.zone.parse(date)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bump_last_viewed_feature_date(user_id, feature_date)
|
||||||
|
Discourse.redis.hset(last_viewed_feature_dates_for_users_key, user_id.to_s, feature_date)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def last_installed_version_key
|
def last_installed_version_key
|
||||||
@ -217,5 +227,9 @@ module DiscourseUpdates
|
|||||||
def new_features_last_seen_key(user_id)
|
def new_features_last_seen_key(user_id)
|
||||||
"new_features_last_seen_user_#{user_id}"
|
"new_features_last_seen_user_#{user_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def last_viewed_feature_dates_for_users_key
|
||||||
|
"last_viewed_feature_dates_for_users_hash"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -129,6 +129,7 @@ module SvgSprite
|
|||||||
"folder-open",
|
"folder-open",
|
||||||
"forward",
|
"forward",
|
||||||
"gavel",
|
"gavel",
|
||||||
|
"gift",
|
||||||
"globe",
|
"globe",
|
||||||
"globe-americas",
|
"globe-americas",
|
||||||
"hand-point-right",
|
"hand-point-right",
|
||||||
|
127
spec/jobs/check_new_features_spec.rb
Normal file
127
spec/jobs/check_new_features_spec.rb
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe Jobs::CheckNewFeatures do
|
||||||
|
def build_feature_hash(id:, created_at:, discourse_version: "2.9.0.beta10")
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
user_id: 89432,
|
||||||
|
emoji: "👤",
|
||||||
|
title: "New fancy feature!",
|
||||||
|
description: "",
|
||||||
|
link: "https://meta.discourse.org/t/-/238821",
|
||||||
|
created_at: created_at.iso8601,
|
||||||
|
updated_at: (created_at + 1.minutes).iso8601,
|
||||||
|
discourse_version: discourse_version
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_meta_new_features_endpoint(*features)
|
||||||
|
stub_request(:get, "https://meta.discourse.org/new-features.json")
|
||||||
|
.to_return(
|
||||||
|
status: 200,
|
||||||
|
body: JSON.dump(features),
|
||||||
|
headers: {
|
||||||
|
"Content-Type" => "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:admin1) { Fabricate(:admin) }
|
||||||
|
fab!(:admin2) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
let(:feature1) do
|
||||||
|
build_feature_hash(
|
||||||
|
id: 35,
|
||||||
|
created_at: 3.days.ago,
|
||||||
|
discourse_version: "2.8.1.beta12"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:feature2) do
|
||||||
|
build_feature_hash(
|
||||||
|
id: 34,
|
||||||
|
created_at: 2.days.ago,
|
||||||
|
discourse_version: "2.8.1.beta13"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pending_feature) do
|
||||||
|
build_feature_hash(
|
||||||
|
id: 37,
|
||||||
|
created_at: 1.day.ago,
|
||||||
|
discourse_version: "2.8.1.beta14"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta13")
|
||||||
|
freeze_time
|
||||||
|
stub_meta_new_features_endpoint(feature1, feature2, pending_feature)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Discourse.redis.del("new_features")
|
||||||
|
Discourse.redis.del("last_viewed_feature_dates_for_users_hash")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "backfills last viewed feature for admins who don't have last viewed feature" do
|
||||||
|
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta12")
|
||||||
|
DiscourseUpdates.update_new_features([feature1].to_json)
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(admin1.id, Time.zone.now.iso8601)
|
||||||
|
|
||||||
|
described_class.new.execute({})
|
||||||
|
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id).iso8601).to eq(feature1[:created_at])
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id).iso8601).to eq(Time.zone.now.iso8601)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "notifies admins about new features that are available in the site's version" do
|
||||||
|
Notification.destroy_all
|
||||||
|
|
||||||
|
described_class.new.execute({})
|
||||||
|
|
||||||
|
expect(admin1.notifications.where(
|
||||||
|
notification_type: Notification.types[:new_features],
|
||||||
|
read: false
|
||||||
|
).count).to eq(1)
|
||||||
|
expect(admin2.notifications.where(
|
||||||
|
notification_type: Notification.types[:new_features],
|
||||||
|
read: false
|
||||||
|
).count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "consolidates new features notifications" do
|
||||||
|
Notification.destroy_all
|
||||||
|
|
||||||
|
described_class.new.execute({})
|
||||||
|
|
||||||
|
notification = admin1.notifications.where(
|
||||||
|
notification_type: Notification.types[:new_features],
|
||||||
|
read: false
|
||||||
|
).first
|
||||||
|
expect(notification).to be_present
|
||||||
|
|
||||||
|
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta14")
|
||||||
|
described_class.new.execute({})
|
||||||
|
|
||||||
|
# old notification is destroyed
|
||||||
|
expect(Notification.find_by(id: notification.id)).to eq(nil)
|
||||||
|
|
||||||
|
notification = admin1.notifications.where(
|
||||||
|
notification_type: Notification.types[:new_features],
|
||||||
|
read: false
|
||||||
|
).first
|
||||||
|
# new notification is created
|
||||||
|
expect(notification).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't notify admins about features they've already seen" do
|
||||||
|
Notification.destroy_all
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(admin1.id, feature2[:created_at])
|
||||||
|
|
||||||
|
described_class.new.execute({})
|
||||||
|
|
||||||
|
expect(admin1.notifications.count).to eq(0)
|
||||||
|
expect(admin2.notifications.where(notification_type: Notification.types[:new_features]).count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
@ -213,4 +213,14 @@ RSpec.describe DiscourseUpdates do
|
|||||||
expect(result[2]["title"]).to eq("Bells")
|
expect(result[2]["title"]).to eq("Bells")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#get_last_viewed_feature_date" do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
it "returns an ActiveSupport::TimeWithZone object" do
|
||||||
|
time = Time.zone.parse("2022-12-13T21:33:59Z")
|
||||||
|
DiscourseUpdates.bump_last_viewed_feature_date(user.id, time)
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(user.id)).to eq(time)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,21 +10,21 @@ RSpec.describe Admin::DashboardController do
|
|||||||
Jobs::VersionCheck.any_instance.stubs(:execute).returns(true)
|
Jobs::VersionCheck.any_instance.stubs(:execute).returns(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def populate_new_features
|
def populate_new_features(date1 = nil, date2 = nil)
|
||||||
sample_features = [
|
sample_features = [
|
||||||
{
|
{
|
||||||
"id" => "1",
|
"id" => "1",
|
||||||
"emoji" => "🤾",
|
"emoji" => "🤾",
|
||||||
"title" => "Cool Beans",
|
"title" => "Cool Beans",
|
||||||
"description" => "Now beans are included",
|
"description" => "Now beans are included",
|
||||||
"created_at" => Time.zone.now - 40.minutes
|
"created_at" => date1 || (Time.zone.now - 40.minutes)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" => "2",
|
"id" => "2",
|
||||||
"emoji" => "🙈",
|
"emoji" => "🙈",
|
||||||
"title" => "Fancy Legumes",
|
"title" => "Fancy Legumes",
|
||||||
"description" => "Legumes too!",
|
"description" => "Legumes too!",
|
||||||
"created_at" => Time.zone.now - 20.minutes
|
"created_at" => date2 || (Time.zone.now - 20.minutes)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -177,6 +177,7 @@ RSpec.describe Admin::DashboardController do
|
|||||||
sign_in(admin)
|
sign_in(admin)
|
||||||
Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
|
Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
|
||||||
Discourse.redis.del "new_features"
|
Discourse.redis.del "new_features"
|
||||||
|
Discourse.redis.del "last_viewed_feature_dates_for_users_hash"
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is empty by default' do
|
it 'is empty by default' do
|
||||||
@ -217,6 +218,30 @@ RSpec.describe Admin::DashboardController do
|
|||||||
|
|
||||||
expect(json['has_unseen_features']).to eq(false)
|
expect(json['has_unseen_features']).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "sets/bumps the last viewed feature date for the admin" do
|
||||||
|
date1 = 30.minutes.ago
|
||||||
|
date2 = 20.minutes.ago
|
||||||
|
populate_new_features(date1, date2)
|
||||||
|
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(admin.id)).to eq(nil)
|
||||||
|
|
||||||
|
get "/admin/dashboard/new-features.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(admin.id)).to be_within_one_second_of(date2)
|
||||||
|
|
||||||
|
date2 = 10.minutes.ago
|
||||||
|
populate_new_features(date1, date2)
|
||||||
|
|
||||||
|
get "/admin/dashboard/new-features.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(admin.id)).to be_within_one_second_of(date2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't error when there are no new features" do
|
||||||
|
get "/admin/dashboard/new-features.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when logged in as a moderator" do
|
context "when logged in as a moderator" do
|
||||||
@ -234,6 +259,16 @@ RSpec.describe Admin::DashboardController do
|
|||||||
expect(json['new_features'][0]["title"]).to eq("Fancy Legumes")
|
expect(json['new_features'][0]["title"]).to eq("Fancy Legumes")
|
||||||
expect(json['has_unseen_features']).to eq(true)
|
expect(json['has_unseen_features']).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't set last viewed feature date for moderators" do
|
||||||
|
populate_new_features
|
||||||
|
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(moderator.id)).to eq(nil)
|
||||||
|
|
||||||
|
get "/admin/dashboard/new-features.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(DiscourseUpdates.get_last_viewed_feature_date(moderator.id)).to eq(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when logged in as a non-staff user" do
|
context "when logged in as a non-staff user" do
|
||||||
|
@ -38,6 +38,9 @@
|
|||||||
"watching_category_or_tag": {
|
"watching_category_or_tag": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"new_features": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"moved_post": {
|
"moved_post": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user