Filter on allowed user language preferences (#2361)

* Naive approached to timeline filtering

* Convert allowed_languages into a db column

* Allow users to choose languages to see statuses in

* Style list items as two columns

* Add a hint to explain language filtering preference
local
Matt Jankowski 7 years ago committed by Eugen Rochko
parent 3988f2dade
commit f025cc6782
  1. 7
      app/assets/stylesheets/forms.scss
  2. 3
      app/controllers/settings/preferences_controller.rb
  3. 2
      app/models/account.rb
  4. 5
      app/models/status.rb
  5. 10
      app/views/settings/preferences/show.html.haml
  6. 2
      config/locales/simple_form.en.yml
  7. 6
      db/migrate/20170423005413_add_allowed_languages_to_user.rb
  8. 2
      db/schema.rb
  9. 8
      spec/controllers/settings/preferences_controller_spec.rb
  10. 25
      spec/models/status_spec.rb

@ -326,3 +326,10 @@ code {
flex: 0 0 auto; flex: 0 0 auto;
} }
} }
.user_allowed_languages {
li {
float: left;
width: 50%;
}
}

@ -25,7 +25,8 @@ class Settings::PreferencesController < ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:locale :locale,
allowed_languages: []
) )
end end

@ -82,6 +82,8 @@ class Account < ApplicationRecord
prefix: true, prefix: true,
allow_nil: true allow_nil: true
delegate :allowed_languages, to: :user, prefix: false, allow_nil: true
def follow!(other_account) def follow!(other_account)
active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) active_relationships.where(target_account: other_account).first_or_create!(target_account: other_account)
end end

@ -119,6 +119,10 @@ class Status < ApplicationRecord
end end
class << self class << self
def in_allowed_languages(account)
where(language: account.allowed_languages)
end
def as_home_timeline(account) def as_home_timeline(account)
where(account: [account] + account.following) where(account: [account] + account.following)
end end
@ -198,6 +202,7 @@ class Status < ApplicationRecord
def filter_timeline_for_account(query, account) def filter_timeline_for_account(query, account)
query = query.not_excluded_by_account(account) query = query.not_excluded_by_account(account)
query = query.in_allowed_languages(account) if account.allowed_languages.present?
query.merge(account_silencing_filter(account)) query.merge(account_silencing_filter(account))
end end

@ -7,6 +7,16 @@
.fields-group .fields-group
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) } = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
= f.input :allowed_languages,
collection: I18n.available_locales,
wrapper: :with_label,
include_blank: false,
label_method: lambda { |locale| human_locale(locale) },
required: false,
as: :check_boxes,
collection_wrapper_tag: 'ul',
item_wrapper_tag: 'li'
= f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group .fields-group

@ -12,6 +12,8 @@ en:
data: CSV file exported from another Mastodon instance data: CSV file exported from another Mastodon instance
sessions: sessions:
otp: Enter the Two-factor code from your phone or use one of your recovery codes. otp: Enter the Two-factor code from your phone or use one of your recovery codes.
user:
allowed_languages: These languages will be allowed in your public timelines. Languages that are not selected will be filtered out.
labels: labels:
defaults: defaults:
avatar: Avatar avatar: Avatar

@ -0,0 +1,6 @@
class AddAllowedLanguagesToUser < ActiveRecord::Migration[5.0]
def change
add_column :users, :allowed_languages, :string, array: true, default: [], null: false
add_index :users, :allowed_languages, using: :gin
end
end

@ -326,7 +326,9 @@ ActiveRecord::Schema.define(version: 20170425202925) do
t.boolean "otp_required_for_login" t.boolean "otp_required_for_login"
t.datetime "last_emailed_at" t.datetime "last_emailed_at"
t.string "otp_backup_codes", array: true t.string "otp_backup_codes", array: true
t.string "allowed_languages", default: [], null: false, array: true
t.index ["account_id"], name: "index_users_on_account_id", using: :btree t.index ["account_id"], name: "index_users_on_account_id", using: :btree
t.index ["allowed_languages"], name: "index_users_on_allowed_languages", using: :gin
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree

@ -3,7 +3,7 @@ require 'rails_helper'
describe Settings::PreferencesController do describe Settings::PreferencesController do
render_views render_views
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user, allowed_languages: []) }
before do before do
sign_in user, scope: :user sign_in user, scope: :user
@ -18,10 +18,12 @@ describe Settings::PreferencesController do
describe 'PUT #update' do describe 'PUT #update' do
it 'updates the user record' do it 'updates the user record' do
put :update, params: { user: { locale: 'en' } } put :update, params: { user: { locale: 'en', allowed_languages: ['es', 'fr'] } }
expect(response).to redirect_to(settings_preferences_path) expect(response).to redirect_to(settings_preferences_path)
expect(user.reload.locale).to eq 'en' user.reload
expect(user.locale).to eq 'en'
expect(user.allowed_languages).to eq ['es', 'fr']
end end
it 'updates user settings' do it 'updates user settings' do

@ -251,6 +251,31 @@ RSpec.describe Status, type: :model do
expect(results).not_to include(muted_status) expect(results).not_to include(muted_status)
end end
context 'with language preferences' do
it 'excludes statuses in languages not allowed by the account user' do
user = Fabricate(:user, allowed_languages: [:en, :es])
@account.update(user: user)
en_status = Fabricate(:status, language: 'en')
es_status = Fabricate(:status, language: 'es')
fr_status = Fabricate(:status, language: 'fr')
results = Status.as_public_timeline(@account)
expect(results).to include(en_status)
expect(results).to include(es_status)
expect(results).not_to include(fr_status)
end
it 'includes all languages when account does not have a user' do
expect(@account.user).to be_nil
en_status = Fabricate(:status, language: 'en')
es_status = Fabricate(:status, language: 'es')
results = Status.as_public_timeline(@account)
expect(results).to include(en_status)
expect(results).to include(es_status)
end
end
context 'where that account is silenced' do context 'where that account is silenced' do
it 'includes statuses from other accounts that are silenced' do it 'includes statuses from other accounts that are silenced' do
@account.update(silenced: true) @account.update(silenced: true)

Loading…
Cancel
Save