diff --git a/src/pages/panel/campaigns/mod.rs b/src/pages/panel/campaigns/mod.rs index 3107413..38f85bb 100644 --- a/src/pages/panel/campaigns/mod.rs +++ b/src/pages/panel/campaigns/mod.rs @@ -32,6 +32,7 @@ pub mod about; pub mod bench; pub mod delete; pub mod new; +pub mod results; pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; @@ -43,6 +44,7 @@ pub fn register_templates(t: &mut tera::Tera) { new::NEW_CAMPAIGN_FORM, bench::BENCH, delete::SUDO_DELETE, + results::CAMPAIGN_RESULTS, ] .iter() { @@ -60,6 +62,7 @@ pub mod routes { pub about: &'static str, pub bench: &'static str, pub delete: &'static str, + pub results: &'static str, } impl Campaigns { pub const fn new() -> Campaigns { @@ -69,6 +72,7 @@ pub mod routes { about: "/survey/campaigns/{uuid}/about", bench: "/survey/campaigns/{uuid}/bench", delete: "/admin/campaigns/{uuid}/delete", + results: "/admin/campaigns/{uuid}/results", } } @@ -84,6 +88,18 @@ pub mod routes { self.about.replace("{uuid}", campaign_id) } + pub fn get_results_route( + &self, + campaign_id: &str, + page: Option, + ) -> String { + let mut res = self.results.replace("{uuid}", campaign_id); + if let Some(page) = page { + res = format!("{res}?page={page}"); + } + res + } + pub const fn get_sitemap() -> [&'static str; 2] { const CAMPAIGNS: Campaigns = Campaigns::new(); [CAMPAIGNS.home, CAMPAIGNS.new] @@ -97,6 +113,7 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) { new::services(cfg); bench::services(cfg); delete::services(cfg); + results::services(cfg); } pub use super::*; @@ -113,14 +130,24 @@ pub struct TemplateCampaign { pub name: String, pub uuid: String, pub route: String, + pub results: String, } impl From for TemplateCampaign { fn from(c: ListCampaignResp) -> Self { let route = crate::PAGES.panel.campaigns.get_about_route(&c.uuid); + let results = crate::PAGES + .panel + .campaigns + .get_results_route(&c.uuid, None); let uuid = c.uuid; let name = c.name; - Self { route, name, uuid } + Self { + route, + name, + uuid, + results, + } } } diff --git a/src/pages/panel/campaigns/results.rs b/src/pages/panel/campaigns/results.rs new file mode 100644 index 0000000..6f5ef35 --- /dev/null +++ b/src/pages/panel/campaigns/results.rs @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; +use std::str::FromStr; + +use actix_web::http::header::ContentType; +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use tera::Context; +use uuid::Uuid; + +use crate::api::v1::admin::campaigns::{runners, ResultsPage, SurveyResponse}; +use crate::errors::ServiceError; +use crate::settings::Settings; +use crate::AppData; + +pub use super::*; + +pub struct CampaignResults { + ctx: RefCell, +} + +pub const CAMPAIGN_RESULTS: TemplateFile = + TemplateFile::new("campaign_results", "panel/campaigns/results.html"); + +impl CtxError for CampaignResults { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +const RESUTS_LIMIT: usize = 50; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct ResultsPagePayload { + next_page: Option, + submissions: Vec, +} + +impl ResultsPagePayload { + pub fn new( + submissions: Vec, + current_page: usize, + campaign_id: &Uuid, + ) -> Self { + let next_page = if submissions.len() >= RESUTS_LIMIT { + Some( + PAGES + .panel + .campaigns + .get_results_route(&campaign_id.to_string(), Some(current_page + 1)), + ) + } else { + None + }; + Self { + next_page, + submissions, + } + } +} + +impl CampaignResults { + pub fn new(settings: &Settings, payload: Option) -> Self { + let ctx = RefCell::new(context(settings, "Results")); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, &payload); + } + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(CAMPAIGN_RESULTS.name, &self.ctx.borrow()) + .unwrap() + } +} + +#[actix_web_codegen_const_routes::get(path = "PAGES.panel.campaigns.results")] +pub async fn results( + id: Identity, + data: AppData, + path: web::Path, + query: web::Query, +) -> PageResult { + match Uuid::from_str(&path) { + Err(_) => Err(PageError::new( + CampaignResults::new(&data.settings, None), + ServiceError::CampaignDoesntExist, + )), + Ok(uuid) => { + let username = id.identity().unwrap(); + let page = query.page(); + + let results = + runners::get_results(&username, &uuid, &data, page, RESUTS_LIMIT) + .await + .map_err(|e| { + PageError::new(CampaignResults::new(&data.settings, None), e) + })?; + let payload = ResultsPagePayload::new(results, page, &uuid); + + let results_page = + CampaignResults::new(&data.settings, Some(payload)).render(); + let html = ContentType::html(); + Ok(HttpResponse::Ok().content_type(html).body(results_page)) + } + } +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(results); +} diff --git a/templates/panel/campaigns/results.html b/templates/panel/campaigns/results.html new file mode 100644 index 0000000..7bf616f --- /dev/null +++ b/templates/panel/campaigns/results.html @@ -0,0 +1,53 @@ +{% extends 'base' %} + +{% block nav %} + {% include "panel_nav" %} +{% endblock nav %} + +{% block body %} + +
+ + + + + + + + + + + + + {% for sub in payload.submissions %} + + + + + + + + + {% endfor %} + +
Submission IDUser IDDevice make (user provided)Device make (detected)ThreadsBenches
{{ sub.id }}{{ sub.user.id }}{{ sub.device_user_provided }}{{ sub.device_software_recognised }}{{ sub.threads }} + + + + + + + {% for b in sub.benches %} + + + + + {% endfor %} + +
DifficultyDuration
{{ b.difficulty }} {{ b.duration }}
+ +
+ Next > +
+ +{% endblock body %}