Skip to content

Commit

Permalink
Implement student endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
nygrenh committed Aug 28, 2024
1 parent 1865eee commit e209f01
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 3 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions services/headless-lms/models/src/code_giveaway_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub async fn give_some_code_to_user(
conn: &mut PgConnection,
code_giveaway_id: Uuid,
user_id: Uuid,
) -> ModelResult<Option<CodeGiveawayCode>> {
) -> ModelResult<CodeGiveawayCode> {
let res = sqlx::query_as!(
CodeGiveawayCode,
r#"
Expand All @@ -140,7 +140,7 @@ RETURNING *
code_giveaway_id,
user_id
)
.fetch_optional(conn)
.fetch_one(conn)
.await?;
Ok(res)
}
Expand Down Expand Up @@ -180,6 +180,28 @@ RETURNING *
Ok(res)
}

pub async fn are_any_codes_left(
conn: &mut PgConnection,
code_giveaway_id: Uuid,
) -> ModelResult<bool> {
let res = sqlx::query!(
r#"
SELECT EXISTS(
SELECT 1
FROM code_giveaway_codes
WHERE code_giveaway_id = $1
AND code_given_to_user_id IS NULL
AND deleted_at IS NULL
LIMIT 1
)
"#,
code_giveaway_id
)
.fetch_one(conn)
.await?;
Ok(res.exists.unwrap())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
22 changes: 22 additions & 0 deletions services/headless-lms/models/src/course_module_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,28 @@ WHERE user_id = $1
Ok(res)
}

pub async fn get_all_by_user_id_and_course_module_id(
conn: &mut PgConnection,
user_id: Uuid,
course_module_id: Uuid,
) -> ModelResult<Vec<CourseModuleCompletion>> {
let res = sqlx::query_as!(
CourseModuleCompletion,
"
SELECT *
FROM course_module_completions
WHERE user_id = $1
AND course_module_id = $2
AND deleted_at IS NULL
",
user_id,
course_module_id,
)
.fetch_all(conn)
.await?;
Ok(res)
}

pub async fn get_all_by_course_module_instance_and_user_ids(
conn: &mut PgConnection,
course_module_id: Uuid,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Controllers for requests starting with `/api/v0/course-material/code-giveaways`.

use crate::{domain::authorization::skip_authorize, prelude::*};

/**
GET /api/v0/course-material/code-giveaways/:id/given-code - If the user has gotten a code from the giveaway, returns the code. Otherwise returns null.
*/
#[instrument(skip(pool))]
async fn get_given_code_for_code_giveaway(
user: AuthUser,
code_giveaway_id: web::Path<Uuid>,
pool: web::Data<PgPool>,
) -> ControllerResult<web::Json<Option<String>>> {
let mut conn = pool.acquire().await?;
let given_code =
models::code_giveaway_codes::get_code_given_to_user(&mut conn, *code_giveaway_id, user.id)
.await?
.map(|o| o.code);
let token = skip_authorize();
token.authorized_ok(web::Json(given_code))
}

/**
GET /api/v0/course-material/code-giveaways/:id/codes-left - Returns whether there are any codes left in the code giveaway.
*/
#[instrument(skip(pool))]
async fn are_any_codes_left_for_code_giveaway(
code_giveaway_id: web::Path<Uuid>,
pool: web::Data<PgPool>,
user: AuthUser,
) -> ControllerResult<web::Json<bool>> {
let mut conn = pool.acquire().await?;
// If user has not completed the required course module, don't leak the information.
let code_giveaway = models::code_giveaways::get_by_id(&mut conn, *code_giveaway_id).await?;
if let Some(course_module_id) = code_giveaway.course_module_id {
let course_module_completions =
models::course_module_completions::get_all_by_user_id_and_course_module_id(
&mut conn,
user.id,
course_module_id,
)
.await?;

course_module_completions
.iter()
.find(|c| c.passed)
.ok_or_else(|| {
ControllerError::new(
ControllerErrorType::BadRequest,
"You have not completed the required course module.".to_string(),
None,
)
})?;
}
// If the user has already gotten a code, don't leak the information anymore.
let already_given_code =
models::code_giveaway_codes::get_code_given_to_user(&mut conn, *code_giveaway_id, user.id)
.await?;
if already_given_code.is_some() {
return Err(ControllerError::new(
ControllerErrorType::BadRequest,
"You have already gotten a code.".to_string(),
None,
));
}
let codes_left =
models::code_giveaway_codes::are_any_codes_left(&mut conn, *code_giveaway_id).await?;
let token = skip_authorize();
token.authorized_ok(web::Json(codes_left))
}

/**
POST /api/v0/course-material/code-giveaways/:id/claim - Claim a code from a code giveaway. If user has not completed the course module that is a requirement for the code, returns an error.
*/
#[instrument(skip(pool))]
async fn claim_code_from_code_giveaway(
user: AuthUser,
code_giveaway_id: web::Path<Uuid>,
pool: web::Data<PgPool>,
) -> ControllerResult<web::Json<String>> {
let mut conn = pool.acquire().await?;
let token = skip_authorize();
let code_giveaway = models::code_giveaways::get_by_id(&mut conn, *code_giveaway_id).await?;
if !code_giveaway.enabled {
return Err(ControllerError::new(
ControllerErrorType::Forbidden,
"Code giveaway is not enabled.".to_string(),
None,
));
}
if let Some(course_module_id) = code_giveaway.course_module_id {
let course_module_completions =
models::course_module_completions::get_all_by_user_id_and_course_module_id(
&mut conn,
user.id,
course_module_id,
)
.await?;

course_module_completions
.iter()
.find(|c| c.passed)
.ok_or_else(|| {
ControllerError::new(
ControllerErrorType::BadRequest,
"You have not completed the required course module.".to_string(),
None,
)
})?;
} else {
return Err(ControllerError::new(
ControllerErrorType::BadRequest,
"The required course module has not been configured to this code giveaway.".to_string(),
None,
));
}

let already_given_code =
models::code_giveaway_codes::get_code_given_to_user(&mut conn, *code_giveaway_id, user.id)
.await?
.map(|o| o.code);

if let Some(code) = already_given_code {
// This is for a pretty message, in the end a database constraint ensures that only one code can be given to a user.
return token.authorized_ok(web::Json(code));
}

let give_code_result =
models::code_giveaway_codes::give_some_code_to_user(&mut conn, *code_giveaway_id, user.id)
.await;

if let Err(_e) = &give_code_result {
let codes_left =
models::code_giveaway_codes::are_any_codes_left(&mut conn, *code_giveaway_id).await?;
if !codes_left {
return Err(ControllerError::new(
ControllerErrorType::BadRequest,
"The giveaway has ran out of codes.".to_string(),
None,
));
}
}

let code = give_code_result?.code;
token.authorized_ok(web::Json(code))
}

pub fn _add_routes(cfg: &mut ServiceConfig) {
cfg.route(
"code-giveaways/{id}/given-code",
web::get().to(get_given_code_for_code_giveaway),
)
.route(
"code-giveaways/{id}/codes-left",
web::get().to(are_any_codes_left_for_code_giveaway),
)
.route(
"code-giveaways/{id}/claim",
web::post().to(claim_code_from_code_giveaway),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This documents all endpoints. Select a module below for a category.

pub mod chapters;
pub mod chatbot;
pub mod code_giveaways;
pub mod course_instances;
pub mod course_modules;
pub mod courses;
Expand All @@ -33,5 +34,6 @@ pub fn _add_routes(cfg: &mut ServiceConfig) {
.service(web::scope("/oembed").configure(oembed::_add_routes))
.service(web::scope("/course-modules").configure(course_modules::_add_routes))
.service(web::scope("/page_audio").configure(page_audio_files::_add_routes))
.service(web::scope("/chatbot").configure(chatbot::_add_routes));
.service(web::scope("/chatbot").configure(chatbot::_add_routes))
.service(web::scope("/code-giveaways").configure(code_giveaways::_add_routes));
}

0 comments on commit e209f01

Please sign in to comment.