Skip to main content

headless_lms_server/controllers/main_frontend/
proposed_edits.rs

1use std::sync::Arc;
2
3use models::proposed_page_edits::{self, EditProposalInfo, PageProposal, ProposalCount};
4use utoipa::OpenApi;
5
6use crate::{
7    domain::{
8        models_requests::{self, JwtKey},
9        request_id::RequestId,
10    },
11    prelude::*,
12};
13
14#[derive(OpenApi)]
15#[openapi(paths(get_edit_proposals, get_edit_proposal_count, process_edit_proposal))]
16pub(crate) struct MainFrontendProposedEditsApiDoc;
17
18#[derive(Debug, Deserialize)]
19
20pub struct GetEditProposalsQuery {
21    pending: bool,
22    #[serde(flatten)]
23    pagination: Pagination,
24}
25
26/**
27GET `/api/v0/main-frontend/proposed-edits/course/:id?pending=true` - Returns feedback for the given course.
28*/
29#[instrument(skip(pool))]
30#[utoipa::path(
31    get,
32    path = "/course/{course_id}",
33    operation_id = "getEditProposals",
34    tag = "proposed_edits",
35    params(
36        ("course_id" = Uuid, Path, description = "Course id"),
37        ("pending" = bool, Query, description = "Whether to fetch pending proposals"),
38        ("page" = Option<i64>, Query, description = "Page number"),
39        ("limit" = Option<i64>, Query, description = "Page size")
40    ),
41    responses(
42        (status = 200, description = "Edit proposals", body = Vec<PageProposal>)
43    )
44)]
45pub async fn get_edit_proposals(
46    course_id: web::Path<Uuid>,
47    pool: web::Data<PgPool>,
48    query: web::Query<GetEditProposalsQuery>,
49    user: AuthUser,
50) -> ControllerResult<web::Json<Vec<PageProposal>>> {
51    let mut conn = pool.acquire().await?;
52
53    let feedback = proposed_page_edits::get_proposals_for_course(
54        &mut conn,
55        *course_id,
56        query.pending,
57        query.pagination,
58    )
59    .await?;
60
61    let token = authorize(
62        &mut conn,
63        Act::Teach,
64        Some(user.id),
65        Res::Course(*course_id),
66    )
67    .await?;
68    token.authorized_ok(web::Json(feedback))
69}
70
71/**
72GET `/api/v0/main-frontend/proposed-edits/course/:id/count` - Returns the amount of feedback for the given course.
73*/
74#[instrument(skip(pool))]
75#[utoipa::path(
76    get,
77    path = "/course/{course_id}/count",
78    operation_id = "getEditProposalCount",
79    tag = "proposed_edits",
80    params(
81        ("course_id" = Uuid, Path, description = "Course id")
82    ),
83    responses(
84        (status = 200, description = "Edit proposal counts", body = ProposalCount)
85    )
86)]
87pub async fn get_edit_proposal_count(
88    course_id: web::Path<Uuid>,
89    pool: web::Data<PgPool>,
90    user: AuthUser,
91) -> ControllerResult<web::Json<ProposalCount>> {
92    let mut conn = pool.acquire().await?;
93
94    let edit_proposal_count =
95        proposed_page_edits::get_proposal_count_for_course(&mut conn, *course_id).await?;
96
97    let token = authorize(
98        &mut conn,
99        Act::Teach,
100        Some(user.id),
101        Res::Course(*course_id),
102    )
103    .await?;
104    token.authorized_ok(web::Json(edit_proposal_count))
105}
106
107/**
108POST `/api/v0/main-frontend/proposed-edits/process-edit-proposal` - Processes the given edit proposal.
109*/
110#[instrument(skip(pool, app_conf))]
111#[utoipa::path(
112    post,
113    path = "/process-edit-proposal",
114    operation_id = "processEditProposal",
115    tag = "proposed_edits",
116    request_body = EditProposalInfo,
117    responses(
118        (status = 200, description = "Processed edit proposal")
119    )
120)]
121pub async fn process_edit_proposal(
122    request_id: RequestId,
123    proposal: web::Json<EditProposalInfo>,
124    app_conf: web::Data<ApplicationConfiguration>,
125    user: AuthUser,
126    pool: web::Data<PgPool>,
127    jwt_key: web::Data<JwtKey>,
128) -> ControllerResult<HttpResponse> {
129    let mut conn = pool.acquire().await?;
130    let proposal = proposal.into_inner();
131
132    let page_proposal =
133        proposed_page_edits::get_by_id(&mut conn, proposal.page_proposal_id).await?;
134    if page_proposal.page_id != proposal.page_id {
135        return Err(controller_err!(
136            Forbidden,
137            "Proposal does not belong to the requested page".to_string()
138        ));
139    }
140
141    let token = authorize(
142        &mut conn,
143        Act::Edit,
144        Some(user.id),
145        Res::Page(page_proposal.page_id),
146    )
147    .await?;
148
149    proposed_page_edits::process_by_id_and_page_id(
150        &mut conn,
151        page_proposal.page_id,
152        proposal.page_proposal_id,
153        proposal.block_proposals,
154        user.id,
155        models_requests::make_spec_fetcher(
156            app_conf.base_url.clone(),
157            request_id.0,
158            Arc::clone(&jwt_key),
159        ),
160        models_requests::fetch_service_info,
161    )
162    .await?;
163
164    token.authorized_ok(HttpResponse::Ok().finish())
165}
166
167/**
168Add a route for each controller in this module.
169
170The name starts with an underline in order to appear before other functions in the module documentation.
171
172We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
173*/
174pub fn _add_routes(cfg: &mut ServiceConfig) {
175    cfg.route("/course/{course_id}", web::get().to(get_edit_proposals))
176        .route(
177            "/course/{course_id}/count",
178            web::get().to(get_edit_proposal_count),
179        )
180        .route(
181            "/process-edit-proposal",
182            web::post().to(process_edit_proposal),
183        );
184}