feat: show campaign results in web UI

This commit is contained in:
Aravinth Manivannan 2023-01-26 20:58:32 +05:30
parent 79bd99d398
commit ab3d496bca
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
3 changed files with 209 additions and 1 deletions

View file

@ -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<usize>,
) -> 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<ListCampaignResp> 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,
}
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Context>,
}
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<String>,
submissions: Vec<SurveyResponse>,
}
impl ResultsPagePayload {
pub fn new(
submissions: Vec<SurveyResponse>,
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<ResultsPagePayload>) -> 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<String>,
query: web::Query<ResultsPage>,
) -> PageResult<impl Responder, CampaignResults> {
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);
}

View file

@ -0,0 +1,53 @@
{% extends 'base' %}
{% block nav %}
{% include "panel_nav" %}
{% endblock nav %}
{% block body %}
<body class="panel__body">
<main class="panel__container">
<table>
<thead>
<tr>
<th>Submission ID</th>
<th>User ID</th>
<th>Device make (user provided)</th>
<th>Device make (detected)</th>
<th>Threads</th>
<th>Benches</th>
</tr>
</thead>
<tbody>
{% for sub in payload.submissions %}
<tr>
<th>{{ sub.id }}</th>
<th>{{ sub.user.id }}</th>
<th>{{ sub.device_user_provided }}</th>
<th>{{ sub.device_software_recognised }}</th>
<th>{{ sub.threads }}</th>
<th>
<table>
<thead>
<th>Difficulty</th>
<th>Duration</th>
</thead>
<tbody>
{% for b in sub.benches %}
<tr>
<td> {{ b.difficulty }} </td>
<td> {{ b.duration }} </td>
</tr>
{% endfor %}
</tbody>
</table>
</th>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{payload.next_page}}">Next ></a>
</main>
</body>
{% endblock body %}