Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change mount_series endpoint to accept and move existing series #1225

Open
wants to merge 7 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .deployment/monitoring/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ collections:
- name: prometheus.prometheus
version: 0.16.3
- name: grafana.grafana
version: 5.2.0
version: 5.5.0
5 changes: 1 addition & 4 deletions .deployment/monitoring/setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
34313337343261363739653632663736613763613763636561363633653362613238333638313733
3265616634663435390a306566656530373362623562333232373364393261353864636665316339
36303161333735313639333735613132626364346536613133626534633063376139
# The latest grafana ansible role does not support grafana version 11 (or later) for now
# due to braking configuration changes. Please update the grafana ansible collection
# and remove this setting to upgrade to the newest grafana version.
grafana_version: 10.4.3
grafana_version: 11.2.0

tasks:
- name: deploy nginx vhosts
Expand Down
39 changes: 24 additions & 15 deletions backend/src/api/model/realm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod mutations;
pub(crate) use mutations::{
ChildIndex, NewRealm, RemovedRealm, UpdateRealm, UpdatedPermissions,
UpdatedRealmName, RealmSpecifier, RealmLineageComponent, CreateRealmLineageOutcome,
RemoveMountedSeriesOutcome,
};


Expand Down Expand Up @@ -231,6 +232,28 @@ impl Realm {
self.is_user_realm() && self.parent_key.is_none()
}

/// Returns all immediate children of this realm. The children are always
/// ordered by the internal index. If `childOrder` returns an ordering
/// different from `BY_INDEX`, the frontend is supposed to sort the
/// children.
pub(crate) async fn children(&self, context: &Context) -> ApiResult<Vec<Self>> {
let selection = Self::select();
let query = format!(
"select {selection} \
from realms \
where realms.parent = $1 \
order by index",
);
context.db
.query_mapped(
&query,
&[&self.key],
|row| Self::from_row_start(&row),
)
.await?
.pipe(Ok)
}

/// Returns the username of the user owning this realm tree IF it is a user
/// realm. Otherwise returns `None`.
pub(crate) fn owning_user(&self) -> Option<&str> {
Expand Down Expand Up @@ -412,21 +435,7 @@ impl Realm {
/// different from `BY_INDEX`, the frontend is supposed to sort the
/// children.
async fn children(&self, context: &Context) -> ApiResult<Vec<Self>> {
let selection = Self::select();
let query = format!(
"select {selection} \
from realms \
where realms.parent = $1 \
order by index",
);
context.db
.query_mapped(
&query,
&[&self.key],
|row| Self::from_row_start(&row),
)
.await?
.pipe(Ok)
self.children(context).await
}

/// Returns the (content) blocks of this realm.
Expand Down
22 changes: 20 additions & 2 deletions backend/src/api/model/realm/mutations.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use std::collections::{HashMap, HashSet};

use crate::{
api::{Context, Id, err::{ApiResult, invalid_input, map_db_err}},
api::{
err::{invalid_input, map_db_err, ApiResult},
model::block::RemovedBlock, Context, Id,
},
auth::AuthContext,
db::types::Key,
prelude::*, auth::AuthContext,
prelude::*,
};
use super::{Realm, RealmOrder};

Expand Down Expand Up @@ -379,6 +383,13 @@ impl UpdatedRealmName {
block: Some(block),
}
}

pub(crate) fn plain(name: String) -> Self {
Self {
plain: Some(name),
block: None,
}
}
}

#[derive(juniper::GraphQLInputObject)]
Expand Down Expand Up @@ -410,3 +421,10 @@ pub(crate) struct RemovedRealm {
pub struct CreateRealmLineageOutcome {
pub num_created: i32,
}

#[derive(juniper::GraphQLObject)]
#[graphql(Context = Context)]
pub(crate) struct RemoveMountedSeriesOutcome {
pub removed_realm: Option<RemovedRealm>,
pub removed_block: Option<RemovedBlock>,
}
Comment on lines +425 to +430
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there is always exactly one of these fields set to Some. Rather sounds like an enum (Rust) / union (graphql) to me?

7 changes: 6 additions & 1 deletion backend/src/api/model/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,16 @@ impl Node for Series {

#[derive(GraphQLInputObject)]
pub(crate) struct NewSeries {
opencast_id: String,
pub(crate) opencast_id: String,
title: String,
// TODO In the future this `struct` can be extended with additional
// (potentially optional) fields. For now we only need these.
// Since `mountSeries` feels even more like a private API
// in some way, and since passing stuff like metadata isn't trivial either
// I think it's okay to leave it at that for now.
}

#[derive(juniper::GraphQLObject)]
pub struct PreparedSeries {
pub id: Id,
}
91 changes: 90 additions & 1 deletion backend/src/api/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::{
id::Id,
Node,
model::{
series::{Series, NewSeries},
series::{Series, NewSeries, PreparedSeries},
realm::{
ChildIndex,
NewRealm,
Expand All @@ -23,6 +23,7 @@ use super::{
RealmSpecifier,
RealmLineageComponent,
CreateRealmLineageOutcome,
RemoveMountedSeriesOutcome,
},
block::{
BlockValue,
Expand Down Expand Up @@ -275,6 +276,94 @@ impl Mutation {
Ok(CreateRealmLineageOutcome { num_created })
}

async fn prepare_series(series: NewSeries, context: &Context) -> ApiResult<PreparedSeries> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure prepare is the best term to use here. I "suggested" prefigure, which I was also not happy with (mainly because I never heard it before). But prepare to me does not sound like "informing Tobira of its existance", but rather like Tobira already knows about the series. So what's happening here is that Opencast informs Tobira in advance about a series that exists, and that Tobira will learn all details of via the harvest API. I already mentioned the German word "vorankündigen" or just "ankündigen" which i think fits nicely. Translation for this are: announce, notify, herald, pronounce, advertise, bode, presage, prefigure, harbinger. Maybe "herald"? Or just announce_series? I had my reservations regarding "announce" but maybe it's actually fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new API endpoints should have some docs explaining what they do.

if context.auth != AuthContext::TrustedExternal {
return Err(not_authorized!("only trusted external applications can use this mutation"));
}

let series = Series::create(series, context).await?;

Ok(PreparedSeries { id: series.id() })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply return Series from this?

}

async fn add_series_mount_point(
series_oc_id: String,
target_path: String,
context: &Context,
) -> ApiResult<Realm> {
if context.auth != AuthContext::TrustedExternal {
return Err(not_authorized!("only trusted external applications can use this mutation"));
}
Comment on lines +294 to +296
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three lines are repeated a bit. How about adding a method to auth named required_trusted_external() -> Result<()>? Then you can just call context.auth.require_trusted_external()?; everywhere


let series = Series::load_by_opencast_id(series_oc_id, context)
.await?
.ok_or_else(|| invalid_input!("`seriesId` does not refer to a valid series"))?;

let target_realm = Realm::load_by_path(target_path, context)
.await?
.ok_or_else(|| invalid_input!("`targetRealmPath` does not refer to a valid realm"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.ok_or_else(|| invalid_input!("`targetRealmPath` does not refer to a valid realm"))?;
.ok_or_else(|| invalid_input!("`targetPath` does not refer to a valid realm"))?;


BlockValue::add_series(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this, the check from mount_series with the error message "series can only be mounted in empty realms" should probably be executed.

Id::realm(target_realm.key),
0,
NewSeriesBlock {
series: series.id(),
show_title: false,
show_metadata: true,
order: VideoListOrder::NewToOld,
layout: VideoListLayout::Gallery,
},
context,
).await?;

let block = &BlockValue::load_for_realm(target_realm.key, context).await?[0];

Realm::rename(
target_realm.id(),
UpdatedRealmName::from_block(block.id()),
context,
).await
}

async fn remove_series_mount_point(
path: String,
context: &Context,
) -> ApiResult<RemoveMountedSeriesOutcome> {
if context.auth != AuthContext::TrustedExternal {
return Err(not_authorized!("only trusted external applications can use this mutation"));
}

let old_realm = Realm::load_by_path(path, context)
.await?
.ok_or_else(|| invalid_input!("`currentRealmPath` does not refer to a valid realm"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.ok_or_else(|| invalid_input!("`currentRealmPath` does not refer to a valid realm"))?;
.ok_or_else(|| invalid_input!("`path` does not refer to a valid realm"))?;


let blocks = BlockValue::load_for_realm(old_realm.key, context).await?;

if blocks.len() > 1 {
return Err(invalid_input!("series can only be moved if its current realm has no other blocks"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message needs adjusting (its not a "move" API anymore)

}

let mut removed_realm = None;
let mut removed_block = None;

if old_realm.children(context).await?.len() == 0 {
// The realm has no children, so it can be removed.
removed_realm = Some(Realm::remove(old_realm.id(), context).await?);
} else {
// At this point we can be certain that there is only one block, which is the series block.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually no, these checks are not happening in this method, at this point we only know that there is 0 or 1 blocks in this realm and that the realm has some children. If the block has 0 blocks, the blocks[0] expression below panics. And the check whether the block is even mentioning the series is probably also useful? Also, the realm doesn't need to be renamed if it doesn't derive the name from the block.

// Ideally we would restore the previous title, but that's not stored anywhere. So the realm
// gets the name of its path segment.
Realm::rename(
old_realm.id(),
UpdatedRealmName::plain(old_realm.path_segment),
context,
).await?;
removed_block = Some(BlockValue::remove(blocks[0].id(), context).await?);
}

Ok(RemoveMountedSeriesOutcome { removed_realm, removed_block })
}

/// Atomically mount a series into an (empty) realm.
/// Creates all the necessary realms on the path to the target
/// and adds a block with the given series at the leaf.
Comment on lines 367 to 369
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mount_series should probably be changed to just call the three new functions (createRealmLineage, announceSeries, addSeriesMountPoint) to avoid duplicated code. Yes, this means some things are needlessly loaded from the DB, but I really don't think that's a problem. And it means that the actual impl needs to live in an actual method somewhere in another file (model::Realm::create_lineage, model::Series::announce, ...) but that's not a bad idea anyway to reduce the size of this mutation.rs file, which cannot be split up.

Expand Down
Loading
Loading