From c62466515f53527cf14f47d59c392a65f96583ee Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sat, 31 Dec 2022 01:27:53 +0530 Subject: [PATCH] feat: view form submissions --- src/pages/dash/sites/forms/mod.rs | 42 +++ src/pages/dash/sites/forms/view.rs | 283 +++++++++++++++++++++ templates/pages/dash/sites/forms/view.html | 33 +++ 3 files changed, 358 insertions(+) create mode 100644 src/pages/dash/sites/forms/mod.rs create mode 100644 src/pages/dash/sites/forms/view.rs create mode 100644 templates/pages/dash/sites/forms/view.html diff --git a/src/pages/dash/sites/forms/mod.rs b/src/pages/dash/sites/forms/mod.rs new file mode 100644 index 0000000..926a949 --- /dev/null +++ b/src/pages/dash/sites/forms/mod.rs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 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 actix_web::*; + +use super::get_auth_middleware; +pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; + +//pub mod delete; +pub mod list; +pub mod view; + +pub fn register_templates(t: &mut tera::Tera) { + list::DASH_SITE_FORM_HOST_LIST + .register(t) + .expect(list::DASH_SITE_FORM_HOST_LIST.name); + view::DASH_SITE_FORM_VIEW + .register(t) + .expect(view::DASH_SITE_FORM_VIEW.name); + // delete::DASH_SITE_DELETE + // .register(t) + // .expect(delete::DASH_SITE_DELETE.name); +} + +pub fn services(cfg: &mut web::ServiceConfig) { + list::services(cfg); + view::services(cfg); + //delete::services(cfg); +} diff --git a/src/pages/dash/sites/forms/view.rs b/src/pages/dash/sites/forms/view.rs new file mode 100644 index 0000000..df01e05 --- /dev/null +++ b/src/pages/dash/sites/forms/view.rs @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2022 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 actix_identity::Identity; +use actix_web::http::header::ContentType; +use libforms::{FormSubmissionResp, Table}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tera::Context; +use time::{format_description, OffsetDateTime}; + +use super::get_auth_middleware; + +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub const DASH_SITE_FORM_VIEW: TemplateFile = + TemplateFile::new("dash_site_form_view", "pages/dash/sites/forms/view.html"); + +pub struct View { + ctx: RefCell, +} + +impl CtxError for View { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl View { + pub fn new(settings: &Settings, payload: Option) -> Self { + let ctx = RefCell::new(context(settings)); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, &payload); + } + + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DASH_SITE_FORM_VIEW.name, &self.ctx.borrow()) + .unwrap() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct TemplateForms { + pub submissions: Vec, + pub next_page: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq)] +pub struct TemplateSubmission { + pub value: TemplateFormData, + pub delete: String, + pub time: String, + pub id: usize, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq)] +pub struct TemplateFormData { + pub titles: Vec, + pub rows: Vec>, +} + +impl From for TemplateFormData { + fn from(json: Value) -> Self { + let mut titles = Vec::default(); + let mut rows: Vec> = Vec::default(); + match json { + Value::Object(m) => { + let mut row: Vec = Vec::default(); + for (k, v) in m.iter() { + titles.push(k.clone()); + let value = match v { + Value::String(s) => s.clone(), + Value::Null => "-".into(), + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::Array(a) => format!("{:?}", a), + Value::Object(_) => unimplemented!(), + }; + row.push(value); + } + + rows.push(row); + } + _ => unimplemented!(), + } + + Self { titles, rows } + } +} + +impl TemplateSubmission { + fn from_form_submission_resp(f: FormSubmissionResp, host: &str, path: &str) -> Self { + let format = format_description::parse( + "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ + sign:mandatory]:[offset_minute]:[offset_second]", + ) + .unwrap(); + let time = OffsetDateTime::from_unix_timestamp(f.time) + .unwrap() + .format(&format) + .unwrap(); + Self { + value: f.value.unwrap().into(), + id: f.id, + time, + delete: PAGES.dash.site.forms.get_delete(f.id, host, path), + } + } +} + +impl TemplateForms { + pub fn new( + current_page: usize, + mut form_submissions: Vec, + host: &str, + path: &str, + ) -> Self { + let mut submissions = Vec::with_capacity(form_submissions.len()); + for sub in form_submissions.drain(0..) { + let sub = TemplateSubmission::from_form_submission_resp(sub, host, path); + submissions.push(sub); + } + let next_page = PAGES.dash.site.forms.get_view(current_page + 1, host, path); + Self { + next_page, + submissions, + } + } +} +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct FormPage { + page: usize, + host: String, + path: String, +} + +impl FormPage { + pub fn to_table(self) -> (Table, usize) { + ( + Table { + host: self.host, + path: self.path, + }, + self.page, + ) + } +} + +#[actix_web_codegen_const_routes::get( + path = "PAGES.dash.site.forms.view", + wrap = "get_auth_middleware()" +)] +#[tracing::instrument(name = "Dashboard view site form submissions webpage", skip(ctx, id))] +pub async fn get_form_submissions( + ctx: AppCtx, + id: Identity, + payload: web::Query, +) -> PageResult { + let owner = id.identity().unwrap(); + let payload = payload.into_inner(); + let (table, page) = payload.to_table(); + let resp = ctx + .get_all_form_submission(&owner, page, &table) + .await + .unwrap(); + // .map_err(|e| PageError::new(View::new(&ctx.settings, None), e))?; + let payload = TemplateForms::new(page, resp, &table.host, &table.path); + + let page = View::new(&ctx.settings, Some(payload)); + let body = page.render(); + let html = ContentType::html(); + Ok(HttpResponse::Ok().content_type(html).body(body)) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_form_submissions); +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + use serde::{Deserialize, Serialize}; + + use crate::ctx::api::v1::auth::Password; + use crate::ctx::ArcCtx; + use crate::errors::ServiceError; + use crate::pages::dash::sites::add::TemplateAddSite; + use crate::tests; + use crate::*; + + use super::Table; + use super::PAGES; + + #[actix_rt::test] + async fn postgres_dashboard_form_works() { + let (_, ctx) = tests::get_ctx().await; + dashboard_site_form_works(ctx.clone()).await; + } + + async fn dashboard_site_form_works(ctx: ArcCtx) { + const NAME: &str = "testdashsiteformuser"; + const EMAIL: &str = "testdashsiteformuser@foo.com"; + const PASSWORD: &str = "longpassword"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx.clone()).await; + let page = ctx.add_test_site(NAME.into()).await; + + let site_info = Table { + host: page.domain.clone(), + path: format!("/foo/{NAME}"), + }; + + #[derive(Serialize, Deserialize)] + struct Foo { + foo: String, + } + + if let Ok(subs) = ctx.get_all_form_submission(NAME, 0, &site_info).await { + for s in subs.iter() { + let _ = ctx.delete_form_submission(NAME, s.id, &site_info).await; + } + } + let foo = Foo { + foo: "barvalue".into(), + }; + ctx.add_form_submission(NAME, &site_info, &serde_json::to_value(&foo).unwrap()) + .await + .unwrap(); + + // list forms for host + let resp = get_request!( + &app, + &PAGES.dash.site.forms.get_list_host(&site_info.host), + cookies.clone() + ); + assert_eq!(resp.status(), StatusCode::OK); + let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); + assert!(res.contains(&site_info.path)); + + // view form submission + let resp = get_request!( + &app, + &PAGES + .dash + .site + .forms + .get_view(0, &site_info.host, &site_info.path), + cookies.clone() + ); + assert_eq!(resp.status(), StatusCode::OK); + let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap(); + assert!(res.contains("foo")); + assert!(res.contains("barvalue")); + } +} diff --git a/templates/pages/dash/sites/forms/view.html b/templates/pages/dash/sites/forms/view.html new file mode 100644 index 0000000..35dab8c --- /dev/null +++ b/templates/pages/dash/sites/forms/view.html @@ -0,0 +1,33 @@ +{% extends 'base' %}{% block title %} View Data {% endblock title %} {% block nav +%} {% include "auth_nav" %} {% endblock nav %} {% block main %} + +
+
+
+ {% for submission in payload.submissions %} + + + + {% for title in submission.value.titles %} + + {% endfor %} + + + + {% for row in submission.value.rows %} + + + {% for value in row %} + + {% endfor %} + + + + {% endfor %} +
{{ title }}Time
{{ value }}{{ submission.time }}
+ {% endfor %} +
+
+
+ +{% endblock main %}