Skip to content

Commit

Permalink
Make known group labels require 'default' instead of 'en' (#1316)
Browse files Browse the repository at this point in the history
This just makes it work the same as all configuration, which was changed
a while ago. Both `TranslatedString` types were combined into one type.
This commit also adjusts some docs that have been forgotten.

There is a second subtle breaking change: it's only possible to import
labels with languages that Tobira knows about, and not arbitrary two-
letter codes, like before. I don't expect that to be a problem. Tobira
was never able to actually show these in the UI anyway.
  • Loading branch information
owi92 authored Jan 17, 2025
2 parents da131cb + 40b63da commit 4eded8e
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 141 deletions.
8 changes: 4 additions & 4 deletions .deployment/files/known-groups.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ROLE_STUDENT": { "label": { "en": "Students", "de": "Studierende" }, "implies": [], "large": true },
"ROLE_STAFF": { "label": { "en": "Staff", "de": "Angestellte" }, "implies": [], "large": true },
"ROLE_INSTRUCTOR": { "label": { "en": "Lecturers", "de": "Vortragende" }, "implies": ["ROLE_STAFF"], "large": true },
"ROLE_TOBIRA_MODERATOR": { "label": { "en": "Moderators", "de": "Moderierende" }, "implies": ["ROLE_STAFF"], "large": false }
"ROLE_STUDENT": { "label": { "default": "Students", "de": "Studierende" }, "implies": [], "large": true },
"ROLE_STAFF": { "label": { "default": "Staff", "de": "Angestellte" }, "implies": [], "large": true },
"ROLE_INSTRUCTOR": { "label": { "default": "Lecturers", "de": "Vortragende" }, "implies": ["ROLE_STAFF"], "large": true },
"ROLE_TOBIRA_MODERATOR": { "label": { "default": "Moderators", "de": "Moderierende" }, "implies": ["ROLE_STAFF"], "large": false }
}
8 changes: 4 additions & 4 deletions backend/src/api/model/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use juniper::{GraphQLInputObject, GraphQLObject};
use postgres_types::BorrowToSql;
use serde::Serialize;

use crate::{api::{util::TranslatedString, Context, err::ApiResult}, db::util::select};
use crate::{api::{err::ApiResult, Context}, config::TranslatedString, db::util::select};



Expand Down Expand Up @@ -31,9 +31,9 @@ pub(crate) struct AclItem {
#[graphql(context = Context)]
pub(crate) struct RoleInfo {
/// A user-facing label for this role (group or person). If the label does
/// not depend on the language (e.g. a name), `{ "_": "Peter" }` is
/// not depend on the language (e.g. a name), `{ "default": "Peter" }` is
/// returned.
pub label: TranslatedString<String>,
pub label: TranslatedString,

/// For user roles this is `null`. For groups, it defines a list of other
/// group roles that this role implies. I.e. a user with this role always
Expand Down Expand Up @@ -66,7 +66,7 @@ where
known_groups.label,
case when users.display_name is null
then null
else hstore('_', users.display_name)
else hstore('default', users.display_name)
end
)",
);
Expand Down
7 changes: 4 additions & 3 deletions backend/src/api/model/known_roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use meilisearch_sdk::search::{Selectors, MatchingStrategies};
use serde::Deserialize;

use crate::{
api::{Context, err::ApiResult, util::TranslatedString},
prelude::*,
api::{err::ApiResult, Context},
config::TranslatedString,
db::util::{impl_from_db, select},
prelude::*,
};
use super::search::{handle_search_result, measure_search_duration, SearchResults, SearchUnavailable};

Expand All @@ -16,7 +17,7 @@ use super::search::{handle_search_result, measure_search_duration, SearchResults
#[derive(juniper::GraphQLObject)]
pub struct KnownGroup {
pub(crate) role: String,
pub(crate) label: TranslatedString<String>,
pub(crate) label: TranslatedString,
pub(crate) implies: Vec<String>,
pub(crate) large: bool,
}
Expand Down
74 changes: 0 additions & 74 deletions backend/src/api/util.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
use std::{collections::HashMap, fmt};

use bytes::BytesMut;
use fallible_iterator::FallibleIterator;
use juniper::{GraphQLScalar, InputValue, ScalarValue};
use postgres_types::{FromSql, ToSql};

use crate::prelude::*;



macro_rules! impl_object_with_dummy_field {
($ty:ident) => {
Expand All @@ -23,67 +13,3 @@ macro_rules! impl_object_with_dummy_field {
}

pub(crate) use impl_object_with_dummy_field;




/// A string in different languages.
#[derive(Debug, GraphQLScalar)]
#[graphql(
where(T: AsRef<str>),
parse_token(String),
)]
pub struct TranslatedString<T>(pub(crate) HashMap<T, String>);

impl<T: AsRef<str> + fmt::Debug> ToSql for TranslatedString<T> {
fn to_sql(
&self,
_: &postgres_types::Type,
out: &mut BytesMut,
) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
let values = self.0.iter().map(|(k, v)| (k.as_ref(), Some(&**v)));
postgres_protocol::types::hstore_to_sql(values, out)?;
Ok(postgres_types::IsNull::No)
}

fn accepts(ty: &postgres_types::Type) -> bool {
ty.name() == "hstore"
}

postgres_types::to_sql_checked!();
}

impl<'a> FromSql<'a> for TranslatedString<String> {
fn from_sql(
_: &postgres_types::Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
postgres_protocol::types::hstore_from_sql(raw)?
.map(|(k, v)| {
v.map(|v| (k.to_owned(), v.to_owned()))
.ok_or("translated label contained null value in hstore".into())
})
.collect()
.map(Self)
}

fn accepts(ty: &postgres_types::Type) -> bool {
ty.name() == "hstore"
}
}

impl<T: AsRef<str>> TranslatedString<T> {
fn to_output<S: ScalarValue>(&self) -> juniper::Value<S> {
self.0.iter()
.map(|(k, v)| (k.as_ref(), juniper::Value::scalar(v.to_owned())))
.collect::<juniper::Object<S>>()
.pipe(juniper::Value::Object)
}

fn from_input<S: ScalarValue>(input: &InputValue<S>) -> Result<Self, String> {
// I did not want to waste time implementing this now, given that we
// likely never use it.
let _ = input;
todo!("TranslatedString cannot be used as input value yet")
}
}
40 changes: 7 additions & 33 deletions backend/src/cmd/known_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use postgres_types::ToSql;
use serde_json::json;

use crate::{
prelude::*,
api::model::known_roles::KnownGroup,
config::{Config, TranslatedString},
db,
api::{util::TranslatedString, model::known_roles::KnownGroup},
config::Config,
prelude::*,
};

use super::prompt_for_yes;
Expand All @@ -25,7 +25,7 @@ pub(crate) enum Args {
///
/// {
/// "ROLE_LECTURER": {
/// "label": { "en": "Lecturer", "de": "Vortragende" },
/// "label": { "default": "Lecturer", "de": "Vortragende" },
/// "implies": ["ROLE_STAFF"],
/// "large": true
/// }
Expand Down Expand Up @@ -112,10 +112,7 @@ async fn upsert(file: &str, config: &Config, tx: Transaction<'_>) -> Result<()>
.context("failed to deserialize")?;

// Validate
for (role, info) in &groups {
if info.label.is_empty() {
bail!("No label given for {}", role.0);
}
for role in groups.keys() {
if config.auth.is_user_role(&role.0) {
bail!("Role '{}' is a user role according to 'auth.user_role_prefixes'. \
This should be added as user, not as group.", role.0);
Expand All @@ -131,7 +128,7 @@ async fn upsert(file: &str, config: &Config, tx: Transaction<'_>) -> Result<()>
label = excluded.label, \
implies = excluded.implies, \
large = excluded.large";
tx.execute(sql, &[&role, &TranslatedString(info.label), &info.implies, &info.large]).await?;
tx.execute(sql, &[&role, &info.label, &info.implies, &info.large]).await?;
}
tx.commit().await?;

Expand Down Expand Up @@ -185,37 +182,14 @@ async fn clear(tx: Transaction<'_>) -> Result<()> {

#[derive(serde::Deserialize)]
struct GroupData {
label: HashMap<LangCode, String>,
label: TranslatedString,

#[serde(default)]
implies: Vec<Role>,

large: bool,
}

#[derive(Debug, serde::Deserialize, PartialEq, Eq, Hash)]
#[serde(try_from = "&str")]
struct LangCode([u8; 2]);

impl<'a> TryFrom<&'a str> for LangCode {
type Error = &'static str;

fn try_from(v: &'a str) -> std::result::Result<Self, Self::Error> {
if !(v.len() == 2 && v.chars().all(|c| c.is_ascii_alphabetic())) {
return Err("invalid language code: two ASCII letters expected");
}

let bytes = v.as_bytes();
Ok(Self([bytes[0], bytes[1]]))
}
}

impl AsRef<str> for LangCode {
fn as_ref(&self) -> &str {
std::str::from_utf8(&self.0).unwrap()
}
}

#[derive(Debug, serde::Deserialize, PartialEq, Eq, Hash, ToSql)]
#[serde(try_from = "String")]
#[postgres(transparent)]
Expand Down
14 changes: 7 additions & 7 deletions backend/src/config/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ pub(crate) struct GeneralConfig {
/// Example:
///
/// ```
/// initial_consent.title.en = "Terms & Conditions"
/// initial_consent.button.en = "Agree"
/// initial_consent.text.en = """
/// initial_consent.title.default = "Terms & Conditions"
/// initial_consent.button.default = "Agree"
/// initial_consent.text.default = """
/// To use Tobira, you need to agree to our terms and conditions:
/// - [Terms](https://www.our-terms.de)
/// - [Conditions](https://www.our-conditions.de)
Expand All @@ -54,8 +54,8 @@ pub(crate) struct GeneralConfig {
///
/// ```
/// footer_links = [
/// { label = { en = "Example 1" }, link = "https://example.com" },
/// { label = { en = "Example 2" }, link = { en = "https://example.com/en" } },
/// { label = { default = "Example 1" }, link = "https://example.com" },
/// { label = { default = "Example 2" }, link = { default = "https://example.com/en" } },
/// "about",
/// ]
/// ```
Expand All @@ -65,8 +65,8 @@ pub(crate) struct GeneralConfig {
/// Additional metadata that is shown below a video. Example:
///
/// [general.metadata]
/// dcterms.spatial = { en = "Location", de = "Ort" }
/// "http://my.domain/xml/namespace".courseLink = { en = "Course", de = "Kurs"}
/// dcterms.spatial = { default = "Location", de = "Ort" }
/// "http://my.domain/xml/namespace".courseLink = { default = "Course", de = "Kurs"}
///
/// As you can see, this is a mapping of a metadata location (the XML
/// namespace and the name) to a translated label. For the XML namespace
Expand Down
91 changes: 85 additions & 6 deletions backend/src/config/translated_string.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
use std::{collections::HashMap, fmt};
use std::{collections::HashMap, fmt, str::FromStr};
use bytes::BytesMut;
use fallible_iterator::FallibleIterator;
use juniper::{GraphQLScalar, InputValue, ScalarValue};
use postgres_types::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use anyhow::{anyhow, Error};

/// A configurable string specified in different languages. Language 'en' always
/// has to be specified.
#[derive(Serialize, Deserialize, Clone)]
use crate::prelude::*;


/// A string specified in different languages. Entry 'default' is required.
#[derive(Serialize, Deserialize, Clone, GraphQLScalar)]
#[serde(try_from = "HashMap<LangKey, String>")]
pub(crate) struct TranslatedString(HashMap<LangKey, String>);
#[graphql(parse_token(String))]
pub(crate) struct TranslatedString(pub(crate) HashMap<LangKey, String>);

impl TranslatedString {
pub(crate) fn default(&self) -> &str {
&self.0[&LangKey::Default]
}

fn to_output<S: ScalarValue>(&self) -> juniper::Value<S> {
self.0.iter()
.map(|(k, v)| (k.as_ref(), juniper::Value::scalar(v.to_owned())))
.collect::<juniper::Object<S>>()
.pipe(juniper::Value::Object)
}

fn from_input<S: ScalarValue>(input: &InputValue<S>) -> Result<Self, String> {
// I did not want to waste time implementing this now, given that we
// likely never use it.
let _ = input;
todo!("TranslatedString cannot be used as input value yet")
}
}

impl TryFrom<HashMap<LangKey, String>> for TranslatedString {
Expand All @@ -33,7 +54,7 @@ impl fmt::Debug for TranslatedString {
}
}

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LangKey {
#[serde(alias = "*")]
Expand All @@ -47,3 +68,61 @@ impl fmt::Display for LangKey {
self.serialize(f)
}
}

impl AsRef<str> for LangKey {
fn as_ref(&self) -> &str {
match self {
LangKey::Default => "default",
LangKey::En => "en",
LangKey::De => "de",
}
}
}

impl FromStr for LangKey {
type Err = serde::de::value::Error;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Self::deserialize(serde::de::value::BorrowedStrDeserializer::new(s))
}
}

impl ToSql for TranslatedString {
fn to_sql(
&self,
_: &postgres_types::Type,
out: &mut BytesMut,
) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
let values = self.0.iter().map(|(k, v)| (k.as_ref(), Some(v.as_str())));
postgres_protocol::types::hstore_to_sql(values, out)?;
Ok(postgres_types::IsNull::No)
}

fn accepts(ty: &postgres_types::Type) -> bool {
ty.name() == "hstore"
}

postgres_types::to_sql_checked!();
}



impl<'a> FromSql<'a> for TranslatedString {
fn from_sql(
_: &postgres_types::Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
postgres_protocol::types::hstore_from_sql(raw)?
.map(|(k, v)| {
let v = v.ok_or("translated label contained null value in hstore")?;
let k = k.parse()?;
Ok((k, v.to_owned()))
})
.collect()
.map(Self)
}

fn accepts(ty: &postgres_types::Type) -> bool {
ty.name() == "hstore"
}
}
Loading

0 comments on commit 4eded8e

Please sign in to comment.