This commit is contained in:
1 parent
984c7083a1
commit
7afdc4be97
29 files changed
+872
-33
No files matched your search
@@ -508,6 +508,15 @@ workspaces:
|
||||
assert!(config.bootstrap_usernames().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_helm_chart_default_instance_configuration_is_valid() {
|
||||
let values: serde_yaml_ng::Value =
|
||||
serde_yaml_ng::from_str(include_str!("../helm/meguri/values.yaml")).unwrap();
|
||||
let rendered = serde_yaml_ng::to_string(&values["instanceConfig"]).unwrap();
|
||||
let config = Config::parse(&rendered).unwrap();
|
||||
assert!(config.oidc.is_some() && !config.workspaces.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_groups_are_unique_and_sorted_and_the_form_flags_follow_the_configuration() {
|
||||
let c = config();
|
||||
|
||||
@@ -127,6 +127,9 @@ pub struct NewUser {
|
||||
pub email: Option<String>,
|
||||
#[serde(default)]
|
||||
pub groups: Vec<String>,
|
||||
/// Scheduling country (ISO 3166-1 alpha-2); defaults to CY like every new account.
|
||||
#[serde(default)]
|
||||
pub country: Option<String>,
|
||||
}
|
||||
|
||||
/// Creates a password account. `actor` is None only for trusted startup seeding; the request
|
||||
@@ -158,6 +161,19 @@ pub async fn create_password_user(
|
||||
let hash = service::hash_password(input.password).await?;
|
||||
let mut row = new_user_row(username.clone(), input.display_name, Some(hash));
|
||||
row.email = email;
|
||||
if let Some(country) = input
|
||||
.country
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|c| !c.is_empty())
|
||||
{
|
||||
let country = country.to_ascii_uppercase();
|
||||
ensure(
|
||||
crate::domain::valid_country(&country),
|
||||
crate::i18n::text("err_choose_a_valid_country_code_e79c72"),
|
||||
)?;
|
||||
row.country = country;
|
||||
}
|
||||
match row.clone().insert(&db.db).await {
|
||||
Ok(_) => {}
|
||||
Err(cot::db::DatabaseError::UniqueViolation) => {
|
||||
|
||||
+21
-18
@@ -14,21 +14,24 @@ CY,2026-10-28,Ohi Day
|
||||
CY,2026-12-24,Christmas Eve
|
||||
CY,2026-12-25,Christmas Day
|
||||
CY,2026-12-26,Boxing Day
|
||||
JP,2026-01-01,New Year's Day
|
||||
JP,2026-01-12,Coming of Age Day
|
||||
JP,2026-02-11,National Foundation Day
|
||||
JP,2026-02-23,Emperor's Birthday
|
||||
JP,2026-03-20,Vernal Equinox Day
|
||||
JP,2026-04-29,Showa Day
|
||||
JP,2026-05-03,Constitution Memorial Day
|
||||
JP,2026-05-04,Greenery Day
|
||||
JP,2026-05-05,Children's Day
|
||||
JP,2026-05-06,Substitute Holiday
|
||||
JP,2026-07-20,Marine Day
|
||||
JP,2026-08-11,Mountain Day
|
||||
JP,2026-09-21,Respect for the Aged Day
|
||||
JP,2026-09-22,Citizens' Holiday
|
||||
JP,2026-09-23,Autumnal Equinox Day
|
||||
JP,2026-10-12,Sports Day
|
||||
JP,2026-11-03,Culture Day
|
||||
JP,2026-11-23,Labour Thanksgiving Day
|
||||
GB,2026-01-01,New Year's Day
|
||||
GB,2026-04-03,Good Friday
|
||||
GB,2026-04-06,Easter Monday
|
||||
GB,2026-05-04,Early May bank holiday
|
||||
GB,2026-05-25,Spring bank holiday
|
||||
GB,2026-08-31,Summer bank holiday
|
||||
GB,2026-12-25,Christmas Day
|
||||
GB,2026-12-28,Boxing Day (substitute day)
|
||||
AM,2026-01-01,New Year's Day
|
||||
AM,2026-01-02,New Year holiday
|
||||
AM,2026-01-06,Christmas Day
|
||||
AM,2026-01-28,Army Day
|
||||
AM,2026-03-08,Women's Day
|
||||
AM,2026-04-07,Motherhood and Beauty Day
|
||||
AM,2026-04-24,Genocide Remembrance Day
|
||||
AM,2026-05-01,Labour Day
|
||||
AM,2026-05-09,Victory and Peace Day
|
||||
AM,2026-05-28,Republic Day
|
||||
AM,2026-07-05,Constitution Day
|
||||
AM,2026-09-21,Independence Day
|
||||
AM,2026-12-31,New Year's Eve
|
||||
|
+16
-9
@@ -228,12 +228,14 @@ pub fn service_tiers(monday: NaiveDate, alice: Uuid, carol: Uuid) -> (Vec<Variab
|
||||
}
|
||||
|
||||
/// Demonstration accounts: groups assigned exactly as an instance administrator would.
|
||||
const ACCOUNTS: [(&str, &str, &str, &[&str]); 6] = [
|
||||
("admin", "Instance Admin", "instance_admin", &[]),
|
||||
/// Scheduling countries differ on purpose, so holiday handling is visible in the examples.
|
||||
const ACCOUNTS: [(&str, &str, &str, &str, &[&str]); 6] = [
|
||||
("admin", "Instance Admin", "instance_admin", "CY", &[]),
|
||||
(
|
||||
"alice",
|
||||
"Alice Morgan",
|
||||
"workspace_admin",
|
||||
"CY",
|
||||
&[
|
||||
"acme-leads",
|
||||
"support-leads",
|
||||
@@ -245,16 +247,18 @@ const ACCOUNTS: [(&str, &str, &str, &[&str]); 6] = [
|
||||
"bob",
|
||||
"Bob Chen",
|
||||
"employee",
|
||||
"GB",
|
||||
&["acme-staff", "regional-staff"],
|
||||
),
|
||||
(
|
||||
"carol",
|
||||
"Carol Davis",
|
||||
"employee",
|
||||
"AM",
|
||||
&["acme-staff", "support-staff", "commerce-staff"],
|
||||
),
|
||||
("dave", "Dave Wilson", "employee", &["acme-staff"]),
|
||||
("erin", "Erin Patel", "employee", &["acme-staff"]),
|
||||
("dave", "Dave Wilson", "employee", "CY", &["acme-staff"]),
|
||||
("erin", "Erin Patel", "employee", "GB", &["acme-staff"]),
|
||||
];
|
||||
|
||||
/// `MEGURI_DEMO_SANDBOXES=n` (0-20) adds empty "Sandbox 1..n" workspaces, each with its own
|
||||
@@ -285,10 +289,12 @@ pub fn config() -> Result<crate::access::Config> {
|
||||
pub fn accounts() -> Vec<crate::demo_runtime::DemoAccount> {
|
||||
ACCOUNTS
|
||||
.iter()
|
||||
.map(|(username, _, role, _)| crate::demo_runtime::DemoAccount {
|
||||
username: (*username).into(),
|
||||
role: (*role).into(),
|
||||
})
|
||||
.map(
|
||||
|(username, _, role, _, _)| crate::demo_runtime::DemoAccount {
|
||||
username: (*username).into(),
|
||||
role: (*role).into(),
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -315,7 +321,7 @@ pub async fn seed(db: &Store) -> Result<()> {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
for (username, display, _, groups) in ACCOUNTS {
|
||||
for (username, display, _, country, groups) in ACCOUNTS {
|
||||
let name = username.to_owned();
|
||||
if query!(UserRow,$username==name).exists(&db.db).await? {
|
||||
continue;
|
||||
@@ -330,6 +336,7 @@ pub async fn seed(db: &Store) -> Result<()> {
|
||||
password: notice.password.clone(),
|
||||
email: None,
|
||||
groups: groups.iter().map(|g| (*g).to_owned()).collect(),
|
||||
country: Some(country.into()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -104,6 +104,41 @@ pub async fn login(
|
||||
Ok((user, token, csrf))
|
||||
}
|
||||
|
||||
/// Employees with schedule-editing rights plan schedules, but the workspace preferences and the
|
||||
/// scheduling directory belong to workspace administrators: name, planning horizon and every
|
||||
/// existing person stay exactly as they were. New email contacts may be added while planning.
|
||||
fn ensure_employee_edit(before: &WorkspaceDocument, after: &WorkspaceDocument) -> Result<()> {
|
||||
let normal = |p: &Person| {
|
||||
(
|
||||
p.display_name.clone(),
|
||||
p.email
|
||||
.as_deref()
|
||||
.map(|e| e.trim().to_lowercase())
|
||||
.filter(|e| !e.is_empty()),
|
||||
p.country
|
||||
.as_deref()
|
||||
.map(|c| c.trim().to_ascii_uppercase())
|
||||
.filter(|c| !c.is_empty())
|
||||
.unwrap_or_else(|| "CY".into()),
|
||||
p.user_id,
|
||||
)
|
||||
};
|
||||
let unchanged = after.name == before.name
|
||||
&& after.horizon_days == before.horizon_days
|
||||
&& before.people.iter().all(|old| {
|
||||
after
|
||||
.people
|
||||
.iter()
|
||||
.find(|p| p.id == old.id)
|
||||
.is_some_and(|new| normal(new) == normal(old))
|
||||
});
|
||||
if unchanged {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_document(
|
||||
db: &Store,
|
||||
actor: Uuid,
|
||||
@@ -115,6 +150,9 @@ pub async fn update_document(
|
||||
.prepare(workspace, revision, actor, Permission::EditSchedules)
|
||||
.await?;
|
||||
document.requests = record.document.requests.clone();
|
||||
if db.owner(workspace, actor).await.is_err() {
|
||||
ensure_employee_edit(&record.document, &document)?;
|
||||
}
|
||||
for request in &mut document.requests {
|
||||
if request.status == RequestStatus::Pending
|
||||
&& request
|
||||
|
||||
Reference in new issue
Block a user