diff --git a/.rubocop.yml b/.rubocop.yml
index 19fdc4e42065fb..76a5f3c2e4c7c4 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -129,6 +129,10 @@ RSpec/FilePath:
- 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder
- 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder
+RSpec/LetSetup:
+ Exclude:
+ - spec/imastodon/**/*
+
# Reason:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject
RSpec/NamedSubject:
diff --git a/app/controllers/api/v1/favourite_tags_controller.rb b/app/controllers/api/v1/favourite_tags_controller.rb
index bd26bf30c061f9..7740e57ded799d 100644
--- a/app/controllers/api/v1/favourite_tags_controller.rb
+++ b/app/controllers/api/v1/favourite_tags_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Api::V1::FavouriteTagsController < Api::BaseController
- before_action :set_account
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index]
before_action :require_user!
@@ -9,57 +8,41 @@ class Api::V1::FavouriteTagsController < Api::BaseController
respond_to :json
def index
- render json: current_favourite_tags
+ current_account = current_user.account
+ orderd_favourite_tags = current_account.favourite_tags.with_order
+
+ render json: orderd_favourite_tags.map(&:to_json_for_api)
end
def create
- tag = find_or_init_tag
- @favourite_tag = FavouriteTag.new(account: @account, tag: tag, visibility: favourite_tag_visibility)
- if @favourite_tag.save
- render json: @favourite_tag.to_json_for_api
- else
- render json: find_fav_tag_by(tag).to_json_for_api, status: 409
+ current_account = current_user.account
+ favourite_tag = current_account.favourite_tags.find_by(name: create_params[:name], visibility: create_params[:visibility])
+
+ if favourite_tag.present?
+ render json: favourite_tag.to_json_for_api, status: 409
+ return
end
+
+ favourite_tag = FavouriteTag.new(account: current_account, name: create_params[:name], visibility: create_params[:visibility])
+ favourite_tag.save!
+ render json: favourite_tag.to_json_for_api
end
def destroy
- tag = find_tag
- @favourite_tag = find_fav_tag_by(tag)
- if @favourite_tag.nil?
- render json: { succeeded: false }, status: 404
+ current_account = current_user.account
+ favourite_tag = current_account.favourite_tags.find_by(id: params[:id])
+ if favourite_tag.nil?
+ render json: { error: 'FavouriteTag is not found' }, status: 404
else
- @favourite_tag.destroy
- render json: { succeeded: true }
+ favourite_tag.destroy!
end
end
private
- def tag_params
- params.permit(:tag, :visibility)
- end
-
- def set_account
- @account = current_user.account
- end
-
- def find_or_init_tag
- Tag.find_or_initialize_by(name: tag_params[:tag])
- end
-
- def find_tag
- Tag.find_by(name: tag_params[:tag])
- end
-
- def find_fav_tag_by(tag)
- @account.favourite_tags.find_by(tag: tag)
- end
-
- def favourite_tag_visibility
- tag_params[:visibility].nil? ? 'public' : tag_params[:visibility]
- end
-
- def current_favourite_tags
- current_account.favourite_tags.with_order.includes(:tag).map(&:to_json_for_api)
+ def create_params
+ params.permit(:name, :visibility)
+ params[:visibility] ||= 'public'
+ params
end
end
diff --git a/app/controllers/settings/favourite_tags_controller.rb b/app/controllers/settings/favourite_tags_controller.rb
index 4f6dafe2864e60..b552ad69953fa9 100644
--- a/app/controllers/settings/favourite_tags_controller.rb
+++ b/app/controllers/settings/favourite_tags_controller.rb
@@ -3,33 +3,42 @@
class Settings::FavouriteTagsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
- before_action :set_account
- before_action :set_favourite_tags, only: [:index, :create]
- before_action :set_favourite_tag, only: [:edit, :update, :destroy]
def index
- @favourite_tag = FavouriteTag.new(tag: Tag.new, visibility: FavouriteTag.visibilities[:public])
+ @favourite_tags = current_account.favourite_tags.with_order
+ @favourite_tag = FavouriteTag.new(visibility: FavouriteTag.visibilities[:public])
end
def edit
- @favourite_tag
+ @favourite_tag = current_account.favourite_tags.find(params[:id])
end
def create
- name = tag_params[:name].delete_prefix('#')
- tag = Tag.find_or_initialize_by(name: name)
- @favourite_tag = FavouriteTag.new(account: @account, tag: tag, visibility: favourite_tag_params[:visibility], order: favourite_tag_params[:order])
+ @favourite_tag = FavouriteTag.new(
+ account: current_account,
+ name: create_params[:name].delete_prefix('#'),
+ order: create_params[:order],
+ visibility: create_params[:visibility]
+ )
+
if @favourite_tag.save
redirect_to settings_favourite_tags_path, notice: I18n.t('generic.changes_saved_msg')
else
+ @favourite_tags = current_account.favourite_tags.with_order
render :index
end
end
def update
- name = tag_params[:name].delete_prefix('#')
- tag = Tag.find_or_initialize_by(name: name)
- if @favourite_tag.update(tag: tag, visibility: favourite_tag_params[:visibility], order: favourite_tag_params[:order])
+ @favourite_tag = current_account.favourite_tags.find(params[:id])
+
+ @favourite_tag.update(
+ name: update_params[:name].delete_prefix('#'),
+ order: update_params[:order],
+ visibility: update_params[:visibility]
+ )
+
+ if @favourite_tag.save
redirect_to settings_favourite_tags_path, notice: I18n.t('generic.changes_saved_msg')
else
render :edit
@@ -37,29 +46,21 @@ def update
end
def destroy
- @favourite_tag.destroy
+ current_account.favourite_tags.destroy(params[:id])
redirect_to settings_favourite_tags_path
end
private
- def tag_params
- params.require(:favourite_tag).require(:tag_attributes).permit(:id, :name)
- end
-
- def favourite_tag_params
- params.require(:favourite_tag).permit(:visibility, :order, { tag_attributes: [:id, :name] })
- end
-
- def set_account
- @account = current_user.account
+ def create_params
+ params.require(:favourite_tag).permit(:name, :visibility, :order)
end
- def set_favourite_tag
- @favourite_tag = @account.favourite_tags.find(params[:id])
+ def update_params
+ params.require(:favourite_tag).permit(:name, :visibility, :order)
end
- def set_favourite_tags
- @favourite_tags = @account.favourite_tags.with_order.includes(:tag)
+ def current_account
+ current_user.account
end
end
diff --git a/app/javascript/mastodon/actions/favourite_tags.js b/app/javascript/mastodon/actions/favourite_tags.js
index dc358d277f3240..68f6a301623409 100644
--- a/app/javascript/mastodon/actions/favourite_tags.js
+++ b/app/javascript/mastodon/actions/favourite_tags.js
@@ -15,7 +15,7 @@ export function refreshFavouriteTags() {
export function addFavouriteTags(tag, visibility) {
return (dispatch, getState) => {
api(getState).post('/api/v1/favourite_tags', {
- tag: tag,
+ name: tag,
visibility: visibility,
}).then(() => {
dispatch(refreshFavouriteTags());
@@ -23,9 +23,9 @@ export function addFavouriteTags(tag, visibility) {
};
}
-export function removeFavouriteTags(tag) {
+export function removeFavouriteTags(id) {
return (dispatch, getState) => {
- api(getState).delete(`/api/v1/favourite_tags/${tag}`).then(() => {
+ api(getState).delete(`/api/v1/favourite_tags/${id}`).then(() => {
dispatch(refreshFavouriteTags());
}).catch(() => {});
};
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx
index 86a347856438b4..5db6a22210f85c 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx
@@ -9,7 +9,8 @@ import Button from '../../../components/button';
const messages = defineMessages({
add_favourite_tags_public: { id: 'tag.add_favourite.public', defaultMessage: 'add in the favourite tags (Public)' },
add_favourite_tags_unlisted: { id: 'tag.add_favourite.unlisted', defaultMessage: 'add in the favourite tags (Unlisted)' },
- remove_favourite_tags: { id: 'tag.remove_favourite', defaultMessage: 'Remove from the favourite tags' },
+ remove_favourite_tags_public: { id: 'tag.remove_favourite.public', defaultMessage: 'Remove from the favourite tags (Public)' },
+ remove_favourite_tags_unlisted: { id: 'tag.remove_favourite.unlisted', defaultMessage: 'Remove from the favourite tags (Unlisted)' },
});
class FavouriteToggle extends React.PureComponent {
@@ -18,7 +19,8 @@ class FavouriteToggle extends React.PureComponent {
tag: PropTypes.string.isRequired,
addFavouriteTags: PropTypes.func.isRequired,
removeFavouriteTags: PropTypes.func.isRequired,
- isRegistered: PropTypes.bool.isRequired,
+ unlistedId: PropTypes.number,
+ publicId: PropTypes.number,
intl: PropTypes.object.isRequired,
};
@@ -34,25 +36,33 @@ class FavouriteToggle extends React.PureComponent {
this.addFavouriteTags('unlisted');
};
- removeFavouriteTags = () => {
- this.props.removeFavouriteTags(this.props.tag);
+ removeFavouriteTags = (id) => {
+ this.props.removeFavouriteTags(id);
};
+ removePublic = () => {
+ this.removeFavouriteTags(this.props.publicId)
+ }
+
+ removeUnlisted = () => {
+ this.removeFavouriteTags(this.props.unlistedId)
+ }
+
render () {
- const { intl, isRegistered } = this.props;
+ const { intl, unlistedId, publicId } = this.props;
return (
- { isRegistered ?
-
-
-
- :
-
-
-
-
- }
+
+ {
+ publicId != null ?
+ :
+ }
+ {
+ unlistedId != null ?
+ :
+ }
+
);
}
diff --git a/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js b/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js
index b2d29ed0c84d09..8abd3a74fe6ac4 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js
@@ -4,7 +4,8 @@ import { addFavouriteTags, removeFavouriteTags } from '../../../actions/favourit
import FavouriteToggle from '../components/favourite_toggle';
const mapStateToProps = (state, { tag }) => ({
- isRegistered: state.getIn(['favourite_tags', 'tags']).some(t => t.get('name') === tag),
+ publicId: state.getIn(['favourite_tags', 'tags']).find(t => t.get('name') === tag && t.get('visibility') === 'public')?.get('id'),
+ unlistedId: state.getIn(['favourite_tags', 'tags']).find(t => t.get('name') === tag && t.get('visibility') === 'unlisted')?.get('id'),
});
const mapDispatchToProps = dispatch => ({
@@ -13,8 +14,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(addFavouriteTags(tag, visibility));
},
- removeFavouriteTags (tag) {
- dispatch(removeFavouriteTags(tag));
+ removeFavouriteTags (id) {
+ dispatch(removeFavouriteTags(id));
},
});
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 24a0002f54960f..842e08c3bfa8bd 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -681,7 +681,8 @@
"tabs_bar.notifications": "Notifications",
"tag.add_favourite.public": "add in the favourite tags (Public)",
"tag.add_favourite.unlisted": "add in the favourite tags (Unlisted)",
- "tag.remove_favourite": "Remove from the favourite tags",
+ "tag.remove_favourite.public": "Remove from the favourite tagss (Public)",
+ "tag.remove_favourite.unlisted": "Remove from the favourite tagsags (Unlisted)",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
diff --git a/app/javascript/mastodon/locales/ja-IM.json b/app/javascript/mastodon/locales/ja-IM.json
index ccdcc3c07662a2..0195cb09a118b0 100644
--- a/app/javascript/mastodon/locales/ja-IM.json
+++ b/app/javascript/mastodon/locales/ja-IM.json
@@ -357,7 +357,8 @@
"tabs_bar.search": "検索",
"tag.add_favourite.public": "スウィーティー☆(公開資料)",
"tag.add_favourite.unlisted": "スウィーティー☆(社内資料)",
- "tag.remove_favourite": "スウィーティーじゃない",
+ "tag.remove_favourite.public": "スウィーティーじゃない(公開)",
+ "tag.remove_favourite.unlisted": "スウィーティーじゃない(未収載)",
"time_remaining.days": "残り{number}日",
"time_remaining.hours": "残り{number}時間",
"time_remaining.minutes": "残り{number}分",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 1f99ca336240fc..89f00cd1a82cb5 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -681,7 +681,8 @@
"tabs_bar.notifications": "通知",
"tag.add_favourite.public": "お気に入り登録(公開)",
"tag.add_favourite.unlisted": "お気に入り登録(未収載)",
- "tag.remove_favourite": "お気に入り解除",
+ "tag.remove_favourite.public": "お気に入り解除(公開)",
+ "tag.remove_favourite.unlisted": "お気に入り解除(未収載)",
"time_remaining.days": "残り{number}日",
"time_remaining.hours": "残り{number}時間",
"time_remaining.minutes": "残り{number}分",
diff --git a/app/lib/friends/favourite_tags_extension.rb b/app/lib/friends/favourite_tags_extension.rb
index e76ba2623b8763..1dd56515aa6ecd 100644
--- a/app/lib/friends/favourite_tags_extension.rb
+++ b/app/lib/friends/favourite_tags_extension.rb
@@ -17,7 +17,7 @@ module FavouriteTagsExtension
def add_default_favourite_tag
DEFAULT_TAGS.each_with_index do |tag_name, i|
- favourite_tags.create!(visibility: 'unlisted', tag: Tag.find_or_create_by!(name: HashtagNormalizer.new.normalize(tag_name)), order: (DEFAULT_TAGS.length - i))
+ favourite_tags.create!(visibility: 'unlisted', name: tag_name, order: (DEFAULT_TAGS.length - i))
end
end
end
diff --git a/app/models/favourite_tag.rb b/app/models/favourite_tag.rb
index 42c85b7464ae36..fd2374a8ee56a8 100644
--- a/app/models/favourite_tag.rb
+++ b/app/models/favourite_tag.rb
@@ -11,23 +11,21 @@
# updated_at :datetime not null
# visibility :integer default("public"), not null
# order :integer default(0), not null
+# name :string
#
class FavouriteTag < ApplicationRecord
enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3 }, _suffix: :visibility
belongs_to :account, optional: false
- belongs_to :tag, optional: false
- accepts_nested_attributes_for :tag
+ belongs_to :tag, optional: true
- validates :tag, uniqueness: { scope: :account }
+ validates :name, format: { with: Tag::HASHTAG_NAME_RE }, uniqueness: { scope: [:account, :visibility] }
validates :visibility, presence: true
validates :order, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
scope :with_order, -> { order(order: :desc, id: :asc) }
- delegate :name, to: :tag
-
def to_json_for_api
{
id: id,
@@ -35,4 +33,10 @@ def to_json_for_api
visibility: visibility,
}
end
+
+ def migrate_tag_name!
+ return if name.present?
+
+ update!(name: tag&.name)
+ end
end
diff --git a/app/views/settings/favourite_tags/_form.html.haml b/app/views/settings/favourite_tags/_form.html.haml
index 0dc806213d85d0..11092d9cbe2078 100644
--- a/app/views/settings/favourite_tags/_form.html.haml
+++ b/app/views/settings/favourite_tags/_form.html.haml
@@ -1,7 +1,6 @@
= simple_form_for [:settings, favourite_tag] do |f|
= render 'shared/error_messages', object: favourite_tag
- = f.simple_fields_for :tag do |ff|
- = ff.input :name, placeholder: t('favourite_tags.name_of_tag')
+ = f.input :name, placeholder: t('favourite_tags.name_of_tag')
= f.input :visibility,
label: t('simple_form.labels.defaults.setting_default_privacy'),
collection: Status.visibilities.keys[0..1],
diff --git a/app/views/settings/favourite_tags/index.html.haml b/app/views/settings/favourite_tags/index.html.haml
index a990a51d132a40..37fe961c008a52 100644
--- a/app/views/settings/favourite_tags/index.html.haml
+++ b/app/views/settings/favourite_tags/index.html.haml
@@ -18,7 +18,7 @@
%tr
%td
= fa_icon 'tag'
- = fav_tag.tag.name
+ = fav_tag.name
%td
= I18n.t("statuses.visibilities.#{fav_tag.visibility}")
%td
diff --git a/config/routes/api.rb b/config/routes/api.rb
index 86d91aef103495..cbf4e4fb0c21b8 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -284,7 +284,7 @@
resources :tags, only: [:index, :show, :update]
end
- resources :favourite_tags, only: [:index, :create, :destroy], param: :tag
+ resources :favourite_tags, only: [:index, :create, :destroy]
end
namespace :v2 do
diff --git a/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb b/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb
new file mode 100644
index 00000000000000..5b76201fb6b708
--- /dev/null
+++ b/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddColumnNameToFavouriteTags < ActiveRecord::Migration[7.0]
+ include Mastodon::MigrationHelpers
+ disable_ddl_transaction!
+
+ def change
+ safety_assured do
+ add_column :favourite_tags, :name, :string
+ change_column_null :favourite_tags, :tag_id, true
+ end
+ end
+end
diff --git a/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb b/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb
new file mode 100644
index 00000000000000..7e2c5471ad1459
--- /dev/null
+++ b/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddIndexAccountVisibilityOnFavouriteTags < ActiveRecord::Migration[7.0]
+ include Mastodon::MigrationHelpers
+ disable_ddl_transaction!
+
+ def change
+ safety_assured do
+ add_index :favourite_tags, [:account_id, :name, :visibility], unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f274359b66afb6..205dfb37875a38 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
+ActiveRecord::Schema[7.0].define(version: 2025_01_05_030426) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -457,8 +457,10 @@
t.datetime "updated_at", precision: nil, null: false
t.integer "visibility", default: 0, null: false
t.bigint "account_id", null: false
- t.bigint "tag_id", null: false
+ t.bigint "tag_id"
t.integer "order", default: 0, null: false
+ t.string "name"
+ t.index ["account_id", "name", "visibility"], name: "index_favourite_tags_on_account_id_and_name_and_visibility", unique: true
t.index ["account_id", "tag_id"], name: "index_favourite_tags_on_account_id_and_tag_id", unique: true
end
diff --git a/lib/tasks/imastodon.rake b/lib/tasks/imastodon.rake
new file mode 100644
index 00000000000000..ba52828ba353c6
--- /dev/null
+++ b/lib/tasks/imastodon.rake
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+namespace :imastodon do
+ task migrate_favourite_tags: :environment do
+ FavouriteTag.all.includes(:tag).find_in_batches do |favourite_tags|
+ Rails.logger.info("imastodon:migrate_favourite_tags: #{favourite_tags.first.id}..#{favourite_tags.last.id}")
+ favourite_tags.each(&:migrate_tag_name!)
+ end
+ end
+end
diff --git a/spec/fabricators/favourite_tags_fabricator.rb b/spec/fabricators/favourite_tags_fabricator.rb
index fb454b0219a6a1..24abb1b724e99b 100644
--- a/spec/fabricators/favourite_tags_fabricator.rb
+++ b/spec/fabricators/favourite_tags_fabricator.rb
@@ -2,7 +2,7 @@
Fabricator(:favourite_tag) do
account
- tag
+ name 'test'
visibility 0
order 0
end
diff --git a/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb b/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb
index 655155d89ba92b..a7af33cd151a0d 100644
--- a/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb
+++ b/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb
@@ -7,7 +7,6 @@
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
- let(:tag) { Fabricate(:tag, name: tag_name) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
@@ -19,50 +18,193 @@
it 'returns http success' do
get :index
expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match(
+ [
+ { id: be_integer, name: 'デレラジ', visibility: 'unlisted' },
+ { id: be_integer, name: 'デレパ', visibility: 'unlisted' },
+ { id: be_integer, name: 'imas_mor', visibility: 'unlisted' },
+ { id: be_integer, name: 'millionradio', visibility: 'unlisted' },
+ { id: be_integer, name: 'sidem', visibility: 'unlisted' },
+ ]
+ )
end
end
describe 'POST #create' do
subject { post :create, params: params }
- let(:scopes) { 'write:statuses' }
- let(:tag_name) { 'dummy_tag' }
- let(:params) do
- {
- tag: tag_name,
- visibility: 'public',
- }
+ let!(:scopes) { 'write:statuses' }
+
+ context '新しいタグをお気に入りタグに登録するとき' do
+ context '公開範囲をpublicで登録するとき' do
+ let!(:params) do
+ {
+ name: 'お気に入りタグ',
+ visibility: 'public',
+ }
+ end
+
+ it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do
+ expect { subject }.to change { user.account.favourite_tags.count }.by(1)
+
+ created = FavouriteTag.last
+ expect(created.name).to eq('お気に入りタグ')
+ expect(created.visibility).to eq('public')
+
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: created.id,
+ name: 'お気に入りタグ',
+ visibility: 'public',
+ })
+ end
+ end
+
+ context '公開範囲をunlistedで登録するとき' do
+ let!(:params) do
+ {
+ name: 'お気に入りタグ',
+ visibility: 'unlisted',
+ }
+ end
+
+ it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do
+ expect { subject }.to change { user.account.favourite_tags.count }.by(1)
+
+ created = FavouriteTag.last
+ expect(created.name).to eq('お気に入りタグ')
+ expect(created.visibility).to eq('unlisted')
+
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: created.id,
+ name: 'お気に入りタグ',
+ visibility: 'unlisted',
+ })
+ end
+ end
+ end
+
+ context '登録しようとしたタグが既にお気に入りタグに登録されているとき' do
+ let!(:already_exists) { Fabricate(:favourite_tag, account: user.account, name: '登録済みのお気に入りタグ', visibility: 'public') }
+
+ context '公開範囲も同じとき' do
+ let!(:params) do
+ {
+ name: '登録済みのお気に入りタグ',
+ visibility: 'public',
+ }
+ end
+
+ it '新しいお気に入りタグのレコードは増えず、ステータスコード409と、既存のお気に入りタグがレスポンスボディとして返る' do
+ expect { subject }.to_not(change { user.account.favourite_tags.count })
+
+ expect(response).to have_http_status(409)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: already_exists.id,
+ name: '登録済みのお気に入りタグ',
+ visibility: 'public',
+ })
+ end
+ end
+
+ context '公開範囲が異なるとき' do
+ let!(:params) do
+ {
+ name: '登録済みのお気に入りタグ',
+ visibility: 'unlisted',
+ }
+ end
+
+ it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do
+ expect { subject }.to change { user.account.favourite_tags.count }.by(1)
+
+ created = FavouriteTag.last
+ expect(created.name).to eq('登録済みのお気に入りタグ')
+ expect(created.visibility).to eq('unlisted')
+
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: created.id,
+ name: '登録済みのお気に入りタグ',
+ visibility: 'unlisted',
+ })
+ end
+ end
end
- context 'when the tag is a new favourite tag' do
- it 'returns http success' do
+ context '英字のタグを大文字小文字違いで登録しようとしたとき' do
+ let!(:already_exists) { Fabricate(:favourite_tag, account: user.account, name: 'already_favourited_tag', visibility: 'public') }
+
+ let!(:params) do
+ {
+ name: 'Already_favourited_tag',
+ visibility: 'public',
+ }
+ end
+
+ it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do
expect { subject }.to change { user.account.favourite_tags.count }.by(1)
+
+ created = FavouriteTag.last
+ expect(created.name).to eq('Already_favourited_tag')
+ expect(created.visibility).to eq('public')
+
expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: created.id,
+ name: 'Already_favourited_tag',
+ visibility: 'public',
+ })
+ end
+ end
+
+ context '公開範囲が指定されなかった場合' do
+ let!(:params) do
+ {
+ name: 'お気に入りタグ',
+ }
end
- it 'responce has created tag' do
+ it '新しいお気に入りタグのレコードが公開範囲publicとして記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do
expect { subject }.to change { user.account.favourite_tags.count }.by(1)
- expect(
- JSON.parse(response.body, symbolize_names: true).except(:id)
- ).to eq({ name: tag_name, visibility: 'public' })
+
+ created = FavouriteTag.last
+ expect(created.name).to eq('お気に入りタグ')
+ expect(created.visibility).to eq('public')
+
+ expect(response).to have_http_status(:success)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ id: created.id,
+ name: 'お気に入りタグ',
+ visibility: 'public',
+ })
end
end
- context 'when the tag has already been favourite.' do
- before do
- Fabricate(:favourite_tag, account: user.account, tag: tag)
+ context 'validでない名前のタグを登録しようとしたとき' do
+ let!(:params) do
+ {
+ name: 'invalid tag name',
+ visibility: 'public',
+ }
end
- it 'returns http 409' do
+ it '新しいお気に入りタグのレコードは増えず、ステータスコード422と、バリデーションエラーの内容が返る' do
expect { subject }.to_not(change { user.account.favourite_tags.count })
- expect(response).to have_http_status(409)
- end
- it 'does not create new favourite_tag' do
- expect { subject }.to_not(change { user.account.favourite_tags.count })
- expect(
- JSON.parse(response.body, symbolize_names: true).except(:id)
- ).to eq({ name: tag_name, visibility: 'public' })
+ expect(response).to have_http_status(422)
+ body = JSON.parse(response.body, symbolize_names: true)
+ expect(body).to match({
+ error: 'Validation failed: Name is invalid',
+ })
end
end
end
@@ -70,42 +212,46 @@
describe 'DELETE #destroy' do
subject { delete :destroy, params: params }
- let(:scopes) { 'write:statuses' }
- let(:params) { { tag: tag_name } }
+ let!(:scopes) { 'write:statuses' }
- context 'when try to destroy the favourite tag' do
- let(:tag_name) { 'dummy_tag' }
+ context '存在するお気に入りタグのIDを指定したとき' do
+ let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) }
+ let!(:params) { { id: favourite_tag.id } }
- before do
- Fabricate(:favourite_tag, account: user.account, tag: tag)
- end
-
- it 'returns http success' do
+ it 'ステータスコード200が返り、お気に入りタグのレコード数が1つ減る' do
expect { subject }.to change { user.account.favourite_tags.count }.by(-1)
- expect(response).to have_http_status(:success)
+ expect(response).to have_http_status(204)
end
+ end
+
+ context '存在しないお気に入りタグのIDを指定したとき' do
+ let!(:params) { { id: 1 } }
+
+ it 'ステータスコード404が返り、お気に入りタグのレコード数は変わらない' do
+ expect { subject }.to_not(change { user.account.favourite_tags.count })
+ expect(response).to have_http_status(404)
- it 'responce has success message by json' do
- expect { subject }.to change { user.account.favourite_tags.count }.by(-1)
expect(
JSON.parse(response.body, symbolize_names: true)
- ).to eq({ succeeded: true })
+ ).to eq({
+ error: 'FavouriteTag is not found',
+ })
end
end
- context 'when try to destroy an unregistered tag' do
- let(:tag_name) { 'unregistered' }
+ context '自分以外の人のお気に入りタグのIDを指定したとき' do
+ let!(:favourite_tag) { Fabricate(:favourite_tag) }
+ let!(:params) { { id: favourite_tag.id } }
- it 'returns http 404' do
+ it 'ステータスコード404が返り、お気に入りタグのレコード数は変わらない' do
expect { subject }.to_not(change { user.account.favourite_tags.count })
expect(response).to have_http_status(404)
- end
- it 'responce has fail message by json' do
- expect { subject }.to_not(change { user.account.favourite_tags.count })
expect(
JSON.parse(response.body, symbolize_names: true)
- ).to eq({ succeeded: false })
+ ).to eq({
+ error: 'FavouriteTag is not found',
+ })
end
end
end
diff --git a/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb b/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb
index 5fa39a2956bb9c..47146dceec79c9 100644
--- a/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb
+++ b/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb
@@ -26,8 +26,7 @@
end
describe 'GET #edit' do
- let(:tag) { Fabricate(:tag, name: 'dummy_tag') }
- let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) }
+ let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) }
context 'when the favourite tag is found.' do
before do
@@ -62,20 +61,12 @@
let(:params) do
{
favourite_tag: {
- tag_attributes: {
- name: tag_name,
- },
+ name: tag_name,
visibility: 'public',
order: 1,
},
}
end
- let!(:tag) { Fabricate(:tag, name: tag_name) }
-
- it 'after create, tag' do
- expect { subject }.to_not change(Tag, :count)
- expect(response).to redirect_to(settings_favourite_tags_path)
- end
it 'after create, favourite tag' do
expect { subject }.to change(FavouriteTag, :count).by(1)
@@ -84,7 +75,7 @@
context 'when the tag has already been favourite.' do
before do
- Fabricate(:favourite_tag, account: user.account, tag: tag)
+ Fabricate(:favourite_tag, account: user.account, name: tag_name)
end
it 'does not create any tags and should render index template' do
@@ -95,8 +86,7 @@
end
describe 'PUT #update' do
- let(:tag) { Fabricate(:tag, name: 'dummy_tag') }
- let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) }
+ let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) }
context 'The favourite tag can update.' do
subject { put :update, params: params }
@@ -105,25 +95,18 @@
{
id: favourite_tag.id,
favourite_tag: {
- tag_attributes: {
- name: "dummy_tag_#{favourite_tag.id}",
- },
+ name: "dummy_tag_#{favourite_tag.id}",
visibility: 'unlisted',
order: 2,
},
}
end
- it 'after update, tag' do
- expect { subject }.to change(Tag, :count).by(1)
- expect(assigns(:favourite_tag).tag.name).to_not eq('dummy_tag')
- expect(response).to redirect_to(settings_favourite_tags_path)
- end
-
it 'after update, favourite tag' do
expect { subject }.to_not change(FavouriteTag, :count)
expect(assigns(:favourite_tag).visibility).to eq('unlisted')
expect(assigns(:favourite_tag).order).to eq(2)
+ expect(assigns(:favourite_tag).name).to eq("dummy_tag_#{favourite_tag.id}")
expect(response).to redirect_to(settings_favourite_tags_path)
end
end
@@ -131,27 +114,24 @@
context 'The favourite tag could not update, because tag has already been registered.' do
subject { put :update, params: params }
- let(:tag_name) { 'dummy_tag2' }
+ let(:tag_name) { 'duplicated' }
let(:params) do
{
id: favourite_tag.id,
favourite_tag: {
- tag_attributes: {
- name: tag_name,
- },
- visibility: 'unlisted',
+ name: tag_name,
+ visibility: 'public',
order: 2,
},
}
end
- let(:tag2) { Fabricate(:tag, name: tag_name) }
before do
- Fabricate(:favourite_tag, account: user.account, tag: tag2)
+ Fabricate(:favourite_tag, account: user.account, name: 'duplicated')
end
it 'does not update any tags and should render edit template' do
- expect { subject }.to_not change(Tag, :count)
+ subject
expect(response).to render_template(:edit)
end
end
@@ -160,19 +140,13 @@
describe 'DELETE #destroy' do
subject { delete :destroy, params: params }
- let(:tag) { Fabricate(:tag, name: 'dummy_tag') }
- let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) }
+ let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, name: 'dummy_tag') }
let(:params) do
{
id: favourite_tag.id,
}
end
- it 'after destroy, tag' do
- expect { subject }.to_not change(Tag, :count)
- expect(response).to redirect_to(settings_favourite_tags_path)
- end
-
it 'after destroy, favourite tag' do
expect { subject }.to change(FavouriteTag, :count).by(-1)
expect(response).to redirect_to(settings_favourite_tags_path)
diff --git a/spec/imastodon/models/favourite_tags_spec.rb b/spec/imastodon/models/favourite_tags_spec.rb
index f92acc7a35fb19..c40def5ebb0ace 100644
--- a/spec/imastodon/models/favourite_tags_spec.rb
+++ b/spec/imastodon/models/favourite_tags_spec.rb
@@ -5,24 +5,44 @@
RSpec.describe FavouriteTag, type: :model do
describe 'validation' do
let(:account) { Fabricate :account }
- let(:tag) { Fabricate(:tag, name: 'valid_tag') }
- it 'valid visibility' do
- expect(described_class.new(account: account, tag: tag, visibility: 0)).to be_valid
- expect(described_class.new(account: account, tag: tag, visibility: 1)).to be_valid
- expect(described_class.new(account: account, tag: tag, visibility: 2)).to be_valid
- expect(described_class.new(account: account, tag: tag, visibility: 3)).to be_valid
- end
+ describe 'visibility' do
+ it '値が0(public)から3(direct)ならvalid' do
+ expect(described_class.new(account: account, name: 'tag', visibility: 0)).to be_valid
+ expect(described_class.new(account: account, name: 'tag', visibility: 1)).to be_valid
+ expect(described_class.new(account: account, name: 'tag', visibility: 2)).to be_valid
+ expect(described_class.new(account: account, name: 'tag', visibility: 3)).to be_valid
+ end
- context 'when visibility is out of ranges' do
- it 'invalid visibility' do
- expect { described_class.new(account: account, tag: tag, visibility: 4) }.to raise_error(ArgumentError)
+ it '値が4(enum上で未定義)ならArgumentErrorをraise' do
+ expect { described_class.new(account: account, name: 'tag', visibility: 4) }.to raise_error(ArgumentError)
end
end
- context 'when the tag is invalid' do
- it 'when tag name is invalid' do
- expect(described_class.new(account: account, tag: Tag.new(name: 'test tag'), visibility: 0)).to_not be_valid
+ describe 'name' do
+ it 'nameは大文字小文字混ざりで作成できる' do
+ # Tagモデルは途中から英字が小文字に正規化されるようになったが、お気に入りタグは大文字小文字混ざりで作成したいという要望があるため
+ favourite_tag = described_class.new(account: account, name: 'Test', visibility: 0)
+
+ expect(favourite_tag).to be_valid
+ end
+
+ it 'お気に入りタグを作成しようとしたとき、そのタグの名前が不正ならinvalid' do
+ expect(described_class.new(account: account, name: 'test tag', visibility: 0)).to_not be_valid
+ end
+
+ it '同じ名前でも公開範囲が異なるならばvalid' do
+ Fabricate(:favourite_tag, account: account, name: 'test', visibility: :public)
+
+ duplicated = described_class.new(account: account, visibility: :unlisted, order: 100, name: 'test')
+ expect(duplicated).to be_valid
+ end
+
+ it '同じ名前、同じ公開範囲で既にお気に入りタグを作成しているならばinvalid' do
+ Fabricate(:favourite_tag, account: account, name: 'test')
+
+ duplicated = described_class.new(account: account, visibility: :public, order: 100, name: 'test')
+ expect(duplicated).to_not be_valid
end
end
end
@@ -42,19 +62,24 @@
it 'returns an array of recent favourite tags ordered by order and id' do
specifieds = [
- Fabricate(:favourite_tag, account: account, order: 9900),
- Fabricate(:favourite_tag, account: account, order: 9800),
- Fabricate(:favourite_tag, account: account, order: 9800),
+ Fabricate(:favourite_tag, account: account, name: 'test1', order: 10),
+ Fabricate(:favourite_tag, account: account, name: 'test2', order: 11),
+ Fabricate(:favourite_tag, account: account, name: 'test3', order: 10),
]
- expect(account.favourite_tags.with_order.limit(3)).to match_array(specifieds)
+ expect(account.favourite_tags.with_order.limit(3)).to eq(
+ [
+ specifieds[1],
+ specifieds[0],
+ specifieds[2],
+ ]
+ )
end
end
end
describe 'expect to_json_for_api' do
let(:account) { Fabricate :account }
- let(:tag) { Tag.new(name: 'test_tag') }
- let!(:favourite_tag) { Fabricate(:favourite_tag, account: account, tag: tag) }
+ let!(:favourite_tag) { Fabricate(:favourite_tag, account: account, name: 'test_tag') }
it 'expect to_json_for_api' do
json = favourite_tag.to_json_for_api
@@ -63,4 +88,32 @@
expect(json[:visibility]).to eq 'public'
end
end
+
+ describe 'migrate_tag_name!' do
+ it 'モデルのnameカラムに値が入っていない場合は関連付けのtagからnameをコピーする' do
+ account = Fabricate(:account)
+ tag = Tag.create!(name: 'tag')
+ favourite_tag = described_class.new(account: account, tag: tag, visibility: :public, name: nil)
+ favourite_tag.save!(validate: false)
+
+ expect(favourite_tag.name).to be_nil
+
+ favourite_tag.migrate_tag_name!
+
+ expect(favourite_tag.name).to eq('tag')
+ end
+
+ it 'モデルのnameカラムに値が入っている場合は変更しない' do
+ account = Fabricate(:account)
+ tag = Tag.create!(name: 'hoge_tag')
+ favourite_tag = described_class.new(account: account, tag: tag, visibility: :public, name: 'tag')
+ favourite_tag.save!
+
+ expect(favourite_tag.name).to eq('tag')
+
+ favourite_tag.migrate_tag_name!
+
+ expect(favourite_tag.name).to eq('tag')
+ end
+ end
end