Merge branch 'add-languages-to-admin-settings' into 'master'

Add languages to admin settings

See merge request framasoft/mobilizon!587
This commit is contained in:
Thomas Citharel 2020-10-07 17:01:35 +02:00
commit 07ab35ab87
15 changed files with 179 additions and 22 deletions

View file

@ -18,6 +18,7 @@ config :mobilizon, :instance,
hostname: "localhost",
registrations_open: false,
registration_email_allowlist: [],
languages: [],
demo: false,
repository: Mix.Project.config()[:source_url],
allow_relay: true,

View file

@ -104,6 +104,15 @@ export const REJECT_RELAY = gql`
${RELAY_FRAGMENT}
`;
export const LANGUAGES = gql`
query {
languages {
code
name
}
}
`;
export const ADMIN_SETTINGS_FRAGMENT = gql`
fragment adminSettingsFragment on AdminSettings {
instanceName
@ -118,6 +127,7 @@ export const ADMIN_SETTINGS_FRAGMENT = gql`
instancePrivacyPolicyUrl
instanceRules
registrationsOpen
instanceLanguages
}
`;
@ -144,6 +154,7 @@ export const SAVE_ADMIN_SETTINGS = gql`
$instancePrivacyPolicyUrl: String
$instanceRules: String
$registrationsOpen: Boolean
$instanceLanguages: [String]
) {
saveAdminSettings(
instanceName: $instanceName
@ -158,6 +169,7 @@ export const SAVE_ADMIN_SETTINGS = gql`
instancePrivacyPolicyUrl: $instancePrivacyPolicyUrl
instanceRules: $instanceRules
registrationsOpen: $registrationsOpen
instanceLanguages: $instanceLanguages
) {
...adminSettingsFragment
}

View file

@ -789,5 +789,9 @@
"Congratulations, your account is now created!": "Congratulations, your account is now created!",
"Now, create your first profile:": "Now, create your first profile:",
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.",
"You will be able to add an avatar and set other options in your account settings.": "You will be able to add an avatar and set other options in your account settings."
"You will be able to add an avatar and set other options in your account settings.": "You will be able to add an avatar and set other options in your account settings.",
"Instance languages": "Instance languages",
"Main languages you/your moderators speak": "Main languages you/your moderators speak",
"Select languages": "Select languages",
"No languages found": "No languages found"
}

View file

@ -388,7 +388,7 @@
"My events": "Mes évènements",
"My groups": "Mes groupes",
"My identities": "Mes identités",
"NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "REMARQUE : les conditions par défaut n'ont pas été vérifiées par un juriste et ne sont donc susceptibles de ne pas offrir une protection juridique complète dans toutes les situations pour un·e administrateur·ice d'instance qui les utilise. Elles ne sont pas non plus spécifiques à tous les pays et juridictions. Si vous n'êtes pas sûr, veuillez consulter un juriste.",
"NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "REMARQUE : les conditions par défaut n'ont pas été vérifiées par un·e juriste et ne sont donc susceptibles de ne pas offrir une protection juridique complète dans toutes les situations pour un·e administrateur·ice d'instance qui les utilise. Elles ne sont pas non plus spécifiques à tous les pays et juridictions. Si vous n'êtes pas sûr, veuillez consulter un·e juriste.",
"Name": "Nom",
"New discussion": "Nouvelle discussion",
"New email": "Nouvelle adresse e-mail",
@ -831,5 +831,9 @@
"Congratulations, your account is now created!": "Bravo, votre compte est dorénavant créé !",
"Now, create your first profile:": "Maintenant, créez votre premier profil :",
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.",
"You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte."
"You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte.",
"Instance languages": "Langues de l'instance",
"Main languages you/your moderators speak": "Principales langues parlées par vous / vos modérateurs",
"Select languages": "Choisissez une langue",
"No languages found": "Aucune langue trouvée"
}

View file

@ -20,6 +20,11 @@ export enum InstancePrivacyType {
CUSTOM = "CUSTOM",
}
export interface ILanguage {
code: string;
name: string;
}
export interface IAdminSettings {
instanceName: string;
instanceDescription: string;
@ -33,4 +38,5 @@ export interface IAdminSettings {
instancePrivacyPolicyUrl: string | null;
instanceRules: string;
registrationsOpen: boolean;
instanceLanguages: string[];
}

View file

@ -43,6 +43,24 @@
<p class="content" v-else>{{ $t("Registration is closed.") }}</p>
</b-switch>
</b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance languages") }}</label>
<small>
{{ $t("Main languages you/your moderators speak") }}
</small>
<b-taginput
v-model="adminSettings.instanceLanguages"
:data="filteredLanguages"
autocomplete
:open-on-focus="true"
field="name"
icon="label"
:placeholder="$t('Select languages')"
@typing="getFilteredLanguages"
>
<template slot="empty">{{ $t("No languages found") }}</template>
</b-taginput>
</div>
<div class="field">
<label class="label has-help">{{ $t("Instance Long Description") }}</label>
<small>
@ -256,32 +274,55 @@
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from "@/graphql/admin";
import { IAdminSettings, InstanceTermsType, InstancePrivacyType } from "../../types/admin.model";
import { Component, Vue, Watch } from "vue-property-decorator";
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS, LANGUAGES } from "@/graphql/admin";
import {
IAdminSettings,
InstanceTermsType,
InstancePrivacyType,
ILanguage,
} from "../../types/admin.model";
import RouteName from "../../router/name";
@Component({
apollo: {
adminSettings: ADMIN_SETTINGS,
languages: LANGUAGES,
},
})
export default class Settings extends Vue {
adminSettings!: IAdminSettings;
languages!: ILanguage[];
filteredLanguages: string[] = [];
InstanceTermsType = InstanceTermsType;
InstancePrivacyType = InstancePrivacyType;
RouteName = RouteName;
async updateSettings() {
@Watch("languages")
setCorrectLanguagesNames(): void {
if (this.languages && this.adminSettings) {
this.adminSettings.instanceLanguages = this.adminSettings.instanceLanguages
.map((code) => this.languageForCode(code))
.filter((language) => language) as string[];
}
}
async updateSettings(): Promise<void> {
const variables = { ...this.adminSettings };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variables.instanceLanguages = variables.instanceLanguages.map((language) => {
return this.codeForLanguage(language);
});
try {
await this.$apollo.mutate({
mutation: SAVE_ADMIN_SETTINGS,
variables: {
...this.adminSettings,
},
variables,
});
this.$notifier.success(this.$t("Admin settings successfully saved.") as string);
} catch (e) {
@ -289,6 +330,32 @@ export default class Settings extends Vue {
this.$notifier.error(this.$t("Failed to save admin settings") as string);
}
}
getFilteredLanguages(text: string): void {
this.filteredLanguages = this.languages
? this.languages
.filter((language: ILanguage) => {
return language.name.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0;
})
.map(({ name }) => name)
: [];
}
codeForLanguage(language: string): string | undefined {
if (this.languages) {
const lang = this.languages.find(({ name }) => name === language);
if (lang) return lang.code;
}
return undefined;
}
languageForCode(codeGiven: string): string | undefined {
if (this.languages) {
const lang = this.languages.find(({ code }) => code === codeGiven);
if (lang) return lang.name;
}
return undefined;
}
}
</script>
<style lang="scss" scoped>

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.{Actors, Admin, Config, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Cldr.Language
alias Mobilizon.Config
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
@ -156,6 +157,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
def get_list_of_languages(_parent, _args, _resolution) do
locale = Gettext.get_locale()
case Language.known_languages(locale) do
data when is_map(data) ->
data = Enum.map(data, fn {code, elem} -> %{code: code, name: elem.standard} end)
{:ok, data}
{:error, err} ->
{:error, err}
end
end
def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
last_public_event_published =
@ -202,11 +216,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
res <-
res
|> Enum.map(fn {key, %Setting{value: value}} ->
case value do
"true" -> {key, true}
"false" -> {key, false}
value -> {key, value}
end
{key, Admin.get_setting_value(value)}
end)
|> Enum.into(%{}),
:ok <- eventually_update_instance_actor(res) do

View file

@ -75,6 +75,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
demo_mode: Config.instance_demo_mode?(),
description: Config.instance_description(),
long_description: Config.instance_long_description(),
languages: Config.instance_languages(),
anonymous: %{
participation: %{
allowed: Config.anonymous_participation?(),

View file

@ -64,6 +64,11 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
end)
end
object :language do
field(:code, :string, description: "The iso-639-3 language code")
field(:name, :string, description: "The language name")
end
object :dashboard do
field(:last_public_event_published, :event, description: "Last public event publish")
field(:number_of_users, :integer, description: "The number of local users")
@ -85,6 +90,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field(:instance_privacy_policy_url, :string)
field(:instance_rules, :string)
field(:registrations_open, :boolean)
field(:instance_languages, list_of(:string))
end
enum :instance_terms_type do
@ -107,6 +113,10 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
resolve(&Admin.list_action_logs/3)
end
field :languages, type: list_of(:language) do
resolve(&Admin.get_list_of_languages/3)
end
field :dashboard, type: :dashboard do
resolve(&Admin.get_dashboard/3)
end
@ -172,6 +182,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
arg(:instance_privacy_policy_url, :string)
arg(:instance_rules, :string)
arg(:registrations_open, :boolean)
arg(:instance_languages, list_of(:string))
resolve(&Admin.save_settings/3)
end

View file

@ -14,6 +14,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:long_description, :string)
field(:contact, :string)
field(:languages, list_of(:string))
field(:registrations_open, :boolean)
field(:registrations_allowlist, :boolean)
field(:demo_mode, :boolean)

View file

@ -80,10 +80,31 @@ defmodule Mobilizon.Admin do
def get_admin_setting_value(group, name, fallback \\ nil)
when is_bitstring(group) and is_bitstring(name) do
case Repo.get_by(Setting, group: group, name: name) do
nil -> fallback
%Setting{value: ""} -> fallback
%Setting{value: nil} -> fallback
%Setting{value: value} -> value
nil ->
fallback
%Setting{value: ""} ->
fallback
%Setting{value: nil} ->
fallback
%Setting{value: value} ->
get_setting_value(value)
end
end
def get_setting_value(value) do
case Jason.decode(value) do
{:ok, val} ->
val
{:error, _} ->
case value do
"true" -> true
"false" -> false
value -> value
end
end
end
@ -116,7 +137,7 @@ defmodule Mobilizon.Admin do
Setting.changeset(%Setting{}, %{
group: group,
name: Atom.to_string(key),
value: to_string(val)
value: convert_to_string(val)
}),
on_conflict: :replace_all,
conflict_target: [:group, :name]
@ -124,4 +145,11 @@ defmodule Mobilizon.Admin do
do_save_setting(transaction, group, rest)
end
defp convert_to_string(val) do
case val do
val when is_list(val) -> Jason.encode!(val)
val -> to_string(val)
end
end
end

View file

@ -5,5 +5,5 @@ defmodule Mobilizon.Cldr do
use Cldr,
locales: ["cs", "de", "en", "es", "fr", "it", "ja", "nl", "pl", "pt", "ru"],
providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime]
providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime, Cldr.Language]
end

View file

@ -99,6 +99,15 @@ defmodule Mobilizon.Config do
)
)
@spec instance_languages :: list(String.t())
def instance_languages,
do:
Mobilizon.Admin.get_admin_setting_value(
"instance",
"instance_languages",
instance_config()[:languages]
)
@spec instance_registrations_allowlist :: list(String.t())
def instance_registrations_allowlist, do: instance_config()[:registration_email_allowlist]
@ -319,7 +328,8 @@ defmodule Mobilizon.Config do
instance_privacy_policy: instance_privacy(),
instance_privacy_policy_type: instance_privacy_type(),
instance_privacy_policy_url: instance_privacy_url(),
instance_rules: instance_rules()
instance_rules: instance_rules(),
instance_languages: instance_languages()
}
end

View file

@ -135,6 +135,7 @@ defmodule Mobilizon.Mixfile do
{:sitemapper, "~> 0.4.0"},
{:xml_builder, "~> 2.1.1", override: true},
{:remote_ip, "~> 0.2.0"},
{:ex_cldr_languages, "~> 0.2.1"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]},

View file

@ -39,6 +39,7 @@
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.10.1", "7e45fd1a26711d644c599e06df132669117bee2ff8e54b93f15e14720f821a9b", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "82a024b160719050e1245a67c62c3be535ddeac70d8c25e0ff1ae69f860da4d1"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.7.0", "d302aa589b4c2a28bc274b1944f879c2505aaf52f9427b04687420d55e317b8b", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b47efd833d5297b3644ff2b59a1d3a0c4b90214847626fbd9e52bcc3db7900b2"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.4", "18143155be8146a3a8b3def620dc4e54b9676143e381519370b2b3fcf8deefbc", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9cf13e6c4a42005a817cd20f82b06697595edee741b60379fbf6a207ca6134b"},
"ex_cldr_languages": {:hex, :ex_cldr_languages, "0.2.1", "4b7d011fc76c563b9d680fb1d1cbc46e485acd50472a22850b7642f463a750f4", [:mix], [{:ex_cldr, "~> 2.2 and >= 2.2.1", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "052bf33032229300d0c1b82bee277611b13ab03d0f1c15f6ce4dd60da0cfd30a"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.4", "1cdeb1f6a22f31e7155edde7d51b3c95ddf6ccf60252a175c10967d4031baebd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.6", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1765b8579b4df5b9fec09bb5506841bf4a51d3dba2c51046e6af05a08fbe7ee1"},
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
"ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"},