Skip to content

Commit

Permalink
Make known group labels require 'default' instead of 'en'
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
LukasKalbertodt committed Jan 16, 2025
1 parent 8a673fc commit 40b63da
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 40b63da

Please sign in to comment.