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
This commit is contained in:
		
				
					committed by
					
						
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							3988f2dade
						
					
				
				
					commit
					f025cc6782
				
			@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user