Sharing tables readonly with other users

- Added sharing feature for table owners to share their tables with
  other registered users.
- Fixed a bug where the wrong entries would be deleted or modified when
  searching or filtering.
This commit is contained in:
Johannes Randerath 2024-09-01 20:14:31 +02:00
parent c6cb3a48bc
commit 61708e5199
22 changed files with 766 additions and 167 deletions

View File

@ -24,4 +24,5 @@ file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "/opt/inventur/inventur/inventur_db/migrations"
dir = "/home/johannes/code/inventur/inventur_db/migrations"

View File

@ -0,0 +1 @@
DROP TABLE shares;

View File

@ -0,0 +1,13 @@
CREATE TABLE shares (
id INTEGER AUTO_INCREMENT UNIQUE NOT NULL,
PRIMARY KEY(id),
sharee_id INTEGER NOT NULL,
FOREIGN KEY (sharee_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
tblid INTEGER NOT NULL,
FOREIGN KEY (tblid) REFERENCES jrtables(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
readonly BOOLEAN NOT NULL DEFAULT true
);

View File

@ -26,6 +26,7 @@ mod jrcolumns;
mod jrentries;
mod jrcells;
mod users;
mod shares;
use dotenvy::dotenv;
use std::env;
@ -38,11 +39,12 @@ use diesel::deserialize;
use diesel::backend::Backend;
use diesel::sql_types::Integer;
use diesel::expression::Expression;
use models::Share;
/// Every column has a type.
/// The default is text, currently number (isize) exists too.
/// Types are mainly used for sorting and can be helpful in client applications.
#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow)]
#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow, Debug)]
#[repr(i32)]
pub enum FIELDTYPE {
TEXT = 0,
@ -75,6 +77,7 @@ impl From<isize> for FIELDTYPE {
/// represents and summarised all relevant data of a table.
/// Standard return type if whole tables should be returned
#[derive(Clone, Debug)]
pub struct Tbl {
/// table id of the represented object
pub tblid: i32,
@ -109,6 +112,14 @@ pub struct User {
pub email: String,
}
#[derive(Clone)]
pub struct ShareCard {
pub tblid: i32,
pub sharee_id: i32,
pub sharee_name: String,
pub readonly: bool,
}
/// Connect to database.
/// Return value must be passed to most of the crate's functions.
pub fn establish_connection() -> MysqlConnection {
@ -130,14 +141,21 @@ pub fn establish_connection() -> MysqlConnection {
/// if there has been another error
///
pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl> {
let tbl = fetch_table(conn, tblid);
if tbl.is_none() ||
tbl.clone().unwrap().1 != uid
{
return None;
}
Some(tbl.unwrap().0)
}
fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{
let tbl = jrtables::get_tbl(conn, tblid);
if tbl.is_err() {
return None;
}
let tbl = tbl.unwrap();
if tbl.owner_id != uid {
return None;
}
let clmns = jrcolumns::get_clmns_of(conn, tblid);
if clmns.is_err() {
return None;
@ -172,7 +190,20 @@ pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl
let data = data;
rows.push(TRow { row_pos: rowid.row_pos, cells: data });
}
Some(Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows })
Some((Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows }, tbl.owner_id))
}
pub fn get_table_shared(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Tbl> {
let tbl = fetch_table(conn, tblid);
let share = shares::is_shared(conn, tblid, uid);
if !share {
return None;
}
let tbl = tbl;
if tbl.is_none() {
return None;
}
Some(tbl.unwrap().0)
}
/// Take a Tbl object, sort it according to parameters and return a sorted Tbl object.
@ -468,3 +499,83 @@ pub fn edit_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_
}
Some(true)
}
pub fn share(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, readonly: &bool, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
println!(":)");
if owner.is_err() ||
owner.unwrap() != uid ||
shares::edit_or_create(conn, tblid, sharee_id, readonly).is_err()
{
return None;
}
Some(true)
}
pub fn unshare(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
owner.unwrap() != uid ||
shares::delete(conn, tblid, sharee_id).is_err()
{
return None;
}
Some(true)
}
pub fn get_sharees(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option<Vec<ShareCard>> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
owner.unwrap() != uid
{
return None;
}
let result = shares::get_shares_tbl(conn, tblid);
if result.is_err() {
return None;
}
let result = result.unwrap();
let mut sharecards = Vec::new();
for share in result {
let uname = users::get_uname(conn, uid);
if uname.is_err() {
continue;
}
sharecards.push(ShareCard { tblid: tblid, sharee_id: share.sharee_id, sharee_name: uname.unwrap(), readonly: share.readonly });
}
return Some(sharecards)
}
pub fn get_shared_tbls(conn: &mut MysqlConnection, uid: i32) -> Option<Vec<ShareCard>> {
let result = shares::get_shares_usr(conn, uid);
if result.is_err() {
return None;
}
let result = result.unwrap();
let uname = users::get_uname(conn, uid);
if uname.is_err() {
return None;
}
let uname = uname.unwrap();
let mut sharecards = Vec::new();
for share in result {
sharecards.push(ShareCard { tblid: share.tblid, sharee_id: uid, sharee_name: uname.clone(), readonly: share.readonly});
}
Some(sharecards)
}
pub fn query_users(conn: &mut MysqlConnection, query: String, uid: i32) -> Option<Vec<User>> {
let result = users::query_users(conn, query);
if users::get_uname(conn, uid).is_err() ||
result.is_err()
{
return None;
}
let result = result.unwrap();
let mut pubusers: Vec<User> = Vec::new();
for user in result {
pubusers.push( User { uid: user.id, uname: user.username, email: user.email } );
}
Some(pubusers)
}

View File

@ -16,7 +16,7 @@
*/
use diesel::prelude::*;
use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users};
use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users, shares};
#[derive(Queryable, Selectable, Identifiable, Associations)]
#[diesel(table_name = crate::schema::jrtables)]
@ -114,3 +114,23 @@ pub struct NewUser {
pub email: String,
}
#[derive(Insertable)]
#[diesel(table_name = shares)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
pub struct NewShare {
pub sharee_id: i32,
pub readonly: bool,
pub tblid: i32,
}
#[derive(Queryable, Selectable, Identifiable, Associations)]
#[diesel(table_name = shares)]
#[diesel(belongs_to(Jrtable, foreign_key = tblid))]
#[diesel(belongs_to(User, foreign_key = sharee_id))]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
pub struct Share {
pub id: i32,
pub sharee_id: i32,
pub tblid: i32,
pub readonly: bool,
}

View File

@ -1,20 +1,3 @@
/* Simple web app using rocket to help maintain inventory data.
* Copyright (C) 2024 Johannes Randerath
*
* 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/>.
*/
// @generated automatically by Diesel CLI.
diesel::table! {
@ -55,6 +38,15 @@ diesel::table! {
}
}
diesel::table! {
shares (id) {
id -> Integer,
sharee_id -> Integer,
tblid -> Integer,
readonly -> Bool,
}
}
diesel::table! {
users (id) {
id -> Integer,
@ -70,11 +62,14 @@ diesel::joinable!(jrcells -> jrentries (jrentry_id));
diesel::joinable!(jrcolumns -> jrtables (jrtable_id));
diesel::joinable!(jrentries -> jrtables (jrtable_id));
diesel::joinable!(jrtables -> users (owner_id));
diesel::joinable!(shares -> jrtables (tblid));
diesel::joinable!(shares -> users (sharee_id));
diesel::allow_tables_to_appear_in_same_query!(
jrcells,
jrcolumns,
jrentries,
jrtables,
shares,
users,
);

53
inventur_db/src/shares.rs Normal file
View File

@ -0,0 +1,53 @@
use crate::models;
use crate::schema;
use models::{NewShare, Share};
use schema::shares::dsl::{id, shares, tblid, sharee_id, readonly};
use diesel::mysql::MysqlConnection;
use diesel::prelude::*;
pub fn edit_or_create(conn: &mut MysqlConnection, tableid: i32, sharee: &i32, ro: &bool) -> Result<usize, diesel::result::Error> {
let share = shares.filter(tblid.eq(tableid))
.filter(sharee_id.eq(sharee))
.select(id)
.first::<i32>(conn);
if share.is_err() {
return diesel::insert_into(schema::shares::table)
.values(&(NewShare { tblid: tableid, sharee_id: *sharee, readonly: *ro }))
.execute(conn);
}
return diesel::update(shares.find(share.unwrap()))
.set(readonly.eq(ro))
.execute(conn);
}
pub fn delete(conn: &mut MysqlConnection, tableid: i32, sharee: &i32) -> Result<usize, diesel::result::Error> {
diesel::delete(shares.filter(tblid.eq(tableid)).filter(sharee_id.eq(*sharee))).execute(conn)
}
pub fn get_shares_tbl(conn: &mut MysqlConnection, tableid: i32) -> Result<Vec<Share>, diesel::result::Error> {
shares
.filter(tblid.eq(tableid))
.select(Share::as_select())
.load::<Share>(conn)
}
pub fn get_shares_usr(conn: &mut MysqlConnection, uid: i32) -> Result<Vec<Share>, diesel::result::Error> {
shares
.filter(sharee_id.eq(uid))
.select(Share::as_select())
.load::<Share>(conn)
}
pub fn is_shared(conn: &mut MysqlConnection, tableid: i32, uid: i32) -> bool {
let share = shares
.filter(sharee_id.eq(uid))
.filter(tblid.eq(tableid))
.execute(conn);
if share.is_err() {
return false;
}
println!("{share:?}");
share.unwrap() >= 1
}

View File

@ -20,7 +20,7 @@ use crate::models;
use crate::schema;
use schema::users::dsl::{users, id, username, email};
use models::NewUser;
use models::{ User, NewUser };
use diesel::prelude::*;
use diesel::mysql::MysqlConnection;
@ -58,4 +58,11 @@ pub fn get_uname(conn: &mut MysqlConnection, uid: i32) -> Result<String, diesel:
.first::<String>(conn)
}
pub fn query_users(conn: &mut MysqlConnection, query: String) -> Result<Vec<User>, diesel::result::Error> {
users
.filter(username.like(format!("{query}%")))
.select(User::as_select())
.load::<User>(conn)
}

View File

@ -38,7 +38,11 @@ use std::env;
use dotenvy::dotenv;
use rocket::Config;
use rocket::figment::providers::Env;
use rocket::http::CookieJar;
use rocket::http::{CookieJar, Status};
use rocket::form::Form;
use rocket::serde::json::Json;
use table::forms::{SearchString, UserQueryResult};
/// Database connection using diesel and rocket_sync_db_pools
#[database("inventur")]
@ -47,13 +51,17 @@ pub struct Db(diesel::MysqlConnection);
/// Home page featuring a preview view of all the user's tables
/// Needs an authenticated user.
#[get("/")]
async fn home(conn: Db, user: AuthUser) -> Template {
async fn home(conn: Db, user: AuthUser) -> Option<Template> {
let uid = user.uid;
let (tids, tnames) = table::get_tids(&conn, uid).await;
let mut cols = Vec::new();
let mut rows = Vec::new();
for tblid in tids.clone() {
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await.unwrap();
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await;
if tbl.is_none() {
return None;
}
let tbl = tbl.unwrap();
cols.push(tbl.column_names.clone());
let mut rws : Vec<Vec<String>> = Vec::new();
for row in tbl.rows {
@ -61,15 +69,51 @@ async fn home(conn: Db, user: AuthUser) -> Template {
}
rows.push(rws.clone());
}
Template::render("home",
context!{
tids: tids,
tnames: tnames,
columns: cols,
rows: rows,
username: user.uname,
email: user.email,
}
let mut shares = conn.run(move |c| inventur_db::get_shared_tbls(c, user.uid)).await;
if shares.is_none() {
shares = Some(Vec::new());
}
let shares = shares.unwrap();
let mut sharetblids = Vec::new();
for tbl in shares {
sharetblids.push(tbl.tblid);
}
let sharetblids = sharetblids;
let mut sharetbls = Vec::new();
for tblid in sharetblids.clone() {
let sharetbl = conn.run(move |c| inventur_db::get_table_shared(c, tblid, uid)).await;
if sharetbl.is_some() {
sharetbls.push(sharetbl.unwrap());
}
}
let sharetbls = sharetbls;
let mut sharedcols = Vec::new();
let mut sharedrows = Vec::new();
let mut sharednms = Vec::new();
for sharetbl in sharetbls {
sharedcols.push(sharetbl.column_names.clone());
let mut rws: Vec<Vec<String>> = Vec::new();
for row in sharetbl.rows {
rws.push(row.cells);
}
sharednms.push(sharetbl.name);
sharedrows.push(rws.clone());
}
Some(
Template::render("home",
context!{
tids: tids,
tnames: tnames,
columns: cols,
rows: rows,
username: user.uname,
email: user.email,
sharedtblids: sharetblids,
sharednms: sharednms,
sharedcols: sharedcols,
sharedrows: sharedrows,
}
)
)
}
@ -93,7 +137,21 @@ async fn favicon() -> Redirect {
Redirect::to(uri!("/img/favicon.ico"))
}
/// Setup app for launch:
#[post("/users", data="<data>")]
async fn search_users(conn: Db, data: Form<SearchString>, user: AuthUser) -> Result<Json<Vec<UserQueryResult>>, Status> {
let result = conn.run(move |c| inventur_db::query_users(c, data.query.clone(), user.uid)).await;
if result.is_none() {
return Err(Status::Forbidden);
}
let result = result.unwrap();
let mut queryresults: Vec<UserQueryResult> = Vec::new();
for u in result {
queryresults.push( UserQueryResult { id: u.uid, username: u.uname } );
}
Ok(rocket::serde::json::Json(queryresults))
}
/// Set app up for launch:
/// Load configuration from a file called .env in the project's root.
/// Use tera templates, connect to mysql db, setup oauth
/// Serve everything related to ...
@ -114,10 +172,11 @@ async fn rocket() -> _ {
.attach(Template::fairing())
.attach(Db::fairing())
.attach(OAuth2::<auth::RanderathIdentity>::fairing("oauth"))
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout])
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table])
.mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon, logout, search_users])
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table, table::edit_share])
.mount("/row", routes![table::new_entry, table::edit_entry, table::delete_entry])
.mount("/column", routes![table::delete_column, table::edit_column, table::create_column])
.mount("/table/readonly", routes![table::table_readonly])
.register("/", catchers![auth::redirect_to_login_401, auth::redirect_to_login_403])
.mount("/img", FileServer::from(relative!("static/img")))
.mount("/css", FileServer::from(relative!("static/css")))

View File

@ -18,7 +18,8 @@
//! Submodule holding structs relevant to handle form data.
use rocket::serde::Deserialize;
use rocket::serde::{Serialize, Deserialize};
//use inventur_db::ShareCard;
#[derive(FromForm)]
pub struct DeleteColumn {
@ -87,4 +88,38 @@ pub struct DeleteTable {
pub tblid: i32,
}
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct Sharee {
id: i32,
username: String,
readonly: bool,
}
impl From<inventur_db::ShareCard> for Sharee {
fn from(sharecard: inventur_db::ShareCard) -> Self {
Sharee { id: sharecard.sharee_id, username: sharecard.sharee_name, readonly: sharecard.readonly}
}
}
#[derive(FromForm, Debug)]
pub struct EditShare {
pub sharees: Vec<i32>,
pub readonly: Vec<bool>,
pub delete: Vec<bool>,
pub tblid: i32,
pub new_user: Option<i32>,
}
#[derive(Serialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct UserQueryResult {
pub id: i32,
pub username: String,
}
#[derive(FromForm, Debug)]
pub struct SearchString {
pub query: String,
}

View File

@ -33,7 +33,7 @@ use rocket::serde::json::Json;
use inventur_db::FIELDTYPE;
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname};
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname, EditShare};
use self::table_view::rocket_uri_macro_table_sec;
/// Delete a table.
@ -88,4 +88,34 @@ pub async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Resu
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Edit or create the sharees of a table
#[post("/share", data="<data>")]
pub async fn edit_share(conn: Db, data: Form<EditShare>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
let tblid = data.tblid;
let ro = data.readonly.clone();
let mut sharees = data.sharees.clone();
if data.new_user.is_some() {
sharees.push(data.new_user.unwrap());
}
let sharees = sharees;
for (i, sharee) in sharees.iter().enumerate() {
let sharee = sharee.clone();
let reado = ro[i].clone();
if conn.run(move |c| inventur_db::share(c, tblid, &sharee, &reado, uid)).await.is_none() {
return Err(Status::Forbidden);
}
}
let del = data.delete.clone();
for (i, todel) in del.iter().enumerate() {
if *todel {
let sharee = sharees[i].clone();
if conn.run(move |c| inventur_db::unshare(c, tblid, &sharee, uid)).await.is_none() {
return Err(Status::Forbidden);
}
}
}
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}

View File

@ -20,29 +20,36 @@
use crate::auth;
use crate::Db;
use crate::table::forms;
use inventur_db;
use auth::AuthUser;
use rocket_dyn_templates::{Template, context};
use rocket::response::Redirect;
use forms::Sharee;
/// View an authenticated user sees visiting the /table/<table id> page
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
let uid = user.uid;
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
if table.is_none() {
return None;
}
let mut table = table.unwrap();
use inventur_db::Tbl;
struct PassTbl {
tbl: Tbl,
searchvalue: String,
searchfields: Vec<i32>,
sortdir: u8,
sortfield: usize,
tname: String,
column_names: Vec<String>,
column_types: Vec<i32>,
rows: Vec<Vec<String>>,
}
async fn sort_and_search(tbl: Tbl, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>) -> PassTbl {
let searchvalue;
let searchfields;
let mut tbl = tbl;
if search_value.is_some() && !search_value.clone().unwrap().eq_ignore_ascii_case("") && search_fields.is_some() {
searchvalue = search_value.unwrap();
searchfields = search_fields.unwrap();
table = inventur_db::search_table(table, searchfields.clone(), searchvalue.clone());
tbl = inventur_db::search_table(tbl, searchfields.clone(), searchvalue.clone());
}else {
if search_value.is_none() {
searchvalue = String::new();
@ -50,7 +57,7 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
searchvalue = search_value.unwrap();
}
if search_fields.is_none() {
searchfields = (0i32..(table.column_names.len() as i32)).collect();
searchfields = (0i32..(tbl.column_names.len() as i32)).collect();
} else {
searchfields = search_fields.unwrap();
@ -69,33 +76,97 @@ pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<
}else {
sortfield = sort_field.unwrap();
}
let table = inventur_db::sort_table(table, sortfield, sortdir);
let column_names = tbl.column_names.clone();
let column_types = tbl.column_types.clone().iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
let rows : Vec<Vec<String>>= tbl.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
PassTbl { tbl: inventur_db::sort_table(tbl.clone(), sortfield, sortdir), searchvalue, searchfields, sortdir, sortfield, tname: tbl.name, column_names, column_types, rows }
}
/// View an authenticated user sees visiting the /table/<table id> page
/// Optional get parametes are the field to sort by, the direction of the sort, the fields to search in and the value to search for.
/// Returns None if the table does not exist, could not be fetched or if the user is not the table's owner.
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
pub async fn table(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
let uid = user.uid;
let table = conn.run(move |c| inventur_db::get_table(c, tid, uid)).await;
if table.is_none() {
return None;
}
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
let table = passtable.tbl;
let tname = table.name;
let column_names = table.column_names;
let column_types = table.column_types.iter().map(|x: &inventur_db::FIELDTYPE| *x as i32).collect::<Vec<i32>>();
let rows : Vec<Vec<String>>= table.rows.iter().map(|v| {let mut r = v.cells.clone(); r.insert(0, v.row_pos.to_string()); r}).collect();
let (tids, tnames) = get_tids(&conn, uid).await;
let sharecards = conn.run(move |c| inventur_db::get_sharees(c, tid, uid)).await;
let mut sharees : Vec<Sharee> = Vec::new();
if sharecards.is_some() {
sharees = sharecards.unwrap().iter().map(|s| Sharee::from((*s).clone())).collect();
}
let sharees = sharees;
Some(
Template::render("table_owned",
context!{
base_url: "/table",
search_value: passtable.searchvalue,
search_fields: passtable.searchfields,
sort_field: passtable.sortfield,
sort_dir: passtable.sortdir,
tblid: tid,
tblname: passtable.tname,
tids: tids,
tnames: tnames,
column_names: passtable.column_names,
column_types: passtable.column_types,
rows: passtable.rows,
username: user.uname,
email: user.email,
shared: sharees,
}
)
)
}
#[get("/<tid>?<sort_dir>&<sort_field>&<search_fields>&<search_value>")]
pub async fn table_readonly(conn: Db, tid: i32, sort_dir: Option<u8>, sort_field: Option<usize>, search_fields: Option<Vec<i32>>, search_value: Option<String>, user: AuthUser) -> Option<Template> {
let uid = user.uid;
let table = conn.run(move |c| inventur_db::get_table_shared(c, tid, uid)).await;
if table.is_none() {
return None;
}
let passtable = sort_and_search(table.unwrap(), sort_dir, sort_field, search_fields, search_value).await;
let table = passtable.tbl;
let (tids, tnames) = get_tids(&conn, uid).await;
Some(
Template::render("table",
Template::render("table_readonly",
context!{
search_value: searchvalue,
search_fields: searchfields,
sort_field: sortfield,
sort_dir: sortdir,
base_url: "/table/readonly",
search_value: passtable.searchvalue,
search_fields: passtable.searchfields,
sort_field: passtable.sortfield,
sort_dir: passtable.sortdir,
tblid: tid,
tblname: tname,
tblname: passtable.tname,
tids: tids,
tnames: tnames,
column_names: column_names,
column_types: column_types,
rows: rows,
column_names: passtable.column_names,
column_types: passtable.column_types,
rows: passtable.rows,
username: user.uname,
email: user.email,
}
)
)
}
/// View to redirect a post request handled to manipulate a table or its display representation back to the (new) table view.

View File

@ -18,64 +18,30 @@
let edit_mode = false;
document.addEventListener("DOMContentLoaded", (event) => {
let rows = document.getElementById("content_table").getElementsByTagName('tbody')[0].rows;
for (let i = 0; i < rows.length; i++)
{
let row = rows[i];
let createClickHandler = function(r)
{
return function()
{
$('#edit_entry_modal').modal('show');
document.getElementById('modal_caller').innerHTML = i+1;
document.getElementById('form_edit_entry_rowpos').value = i+1;
document.getElementById('form_delete_entry_rowpos').value = i+1;
let cells = row.cells;
for (let j = 1; j < cells.length; j++) {
document.getElementById(`form_edit_entry_${j}`).value = cells[j].innerHTML;
}
};
};
row.onclick = createClickHandler(row);
}
});
document.addEventListener("DOMContentLoaded", (event) => {
let rows = document.getElementById("content_table").getElementsByTagName('tbody')[0].rows;
for (let i = 0; i < rows.length; i++)
{
let row = rows[i];
let createClickHandler = function(r)
{
return function()
{
$('#edit_entry_modal').modal('show');
let row_pos = $(this).find('td').first().html();
$('#modal_caller').html(row_pos);
$('#form_edit_entry_rowpos').val(row_pos);
$('#form_delete_entry_rowpos').val(row_pos);
let cells = row.cells;
for (let j = 1; j < cells.length; j++) {
$(`#form_edit_entry_${j}`).val(cells[j].innerHTML);
}
};
};
row.onclick = createClickHandler(row);
}
});
function toggle_edit_tname() {
let span = document.getElementById('tname');
if (!edit_mode) {
span.innerHTML = `
<div class='row'>
<div class='col-auto ml-1 p-2'>
<form id='form_edit_table' action='/table/name/edit' method='post'>
<input name='tblid' value='${tblid}' hidden />
<input type='text' class='form-control' id='tname_edit' name='new_name' value='${tblname}' />
</form>
</div>
<div class='col-auto mt-0 pt-0 pr-1'>
<div class="btn-group" role="group">
<button class="btn btn-secondary" onclick="toggle_edit_tname();" type="button"><i class="bi bi-x"></i></button>
<button class='btn btn-success' type='button' onclick='edit_tname()'>
<i class='bi bi-check'></i>
</button>
</div>
<button class='btn btn-danger' type="button" onclick='confirm_delete("table");'><i class="bi bi-trash3-fill"></i></button>
</div>
<form id="form_delete_table" action="/table/delete" method="post">
<input value="${tblid}" name="tblid" hidden />
</form>`;
edit_mode = true;
document.getElementById('pencil_button_edit_tname').hidden = true;
} else {
document.getElementById('pencil_button_edit_tname').hidden = false;
span.innerHTML = `${tblname}`;
edit_mode = false;
}
}
function edit_tname() {
document.getElementById('form_edit_table').submit();
toggle_edit_tname();
}
function edit_column(clmn_index) {
document.getElementById('form_edit_column_name').value = column_names[clmn_index];
document.getElementById('form_edit_column_type').value = column_types[clmn_index];

71
static/js/table_owned.js Normal file
View File

@ -0,0 +1,71 @@
$(function() {
$('.edit_column_btn').attr('hidden', false);
});
function query_users(querystr) {
let req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let sel = $('#form_share_table_select_new_user');
sel.empty();
let resp = $.parseJSON(this.responseText);
for (user of resp.slice(0, 5)) {
sel
.append($('<li>')
.attr('class', "list-group-item align-content-center")
.text(user.username)
.on('click', function() {
$('#form_share_table_new_user_input')
.val(user.username)
.attr('disabled', true);
$('#form_share_table_new_user_id')
.val(user.id)
.attr('disabled', false);
}));
console.log(sel);
}
}
};
req.open("POST", "/users", true);
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.send("query=" + querystr);
}
function toggle_edit_tname() {
let span = document.getElementById('tname');
if (!edit_mode) {
span.innerHTML = `
<div class='row'>
<div class='col-auto ml-1 p-2'>
<form id='form_edit_table' action='/table/name/edit' method='post'>
<input name='tblid' value='${tblid}' hidden />
<input type='text' class='form-control' id='tname_edit' name='new_name' value='${tblname}' />
</form>
</div>
<div class='col-auto mt-0 pt-0 pr-1'>
<div class="btn-group" role="group">
<button class="btn btn-secondary" onclick="toggle_edit_tname();" type="button"><i class="bi bi-x"></i></button>
<button class='btn btn-success' type='button' onclick='edit_tname()'>
<i class='bi bi-check'></i>
</button>
</div>
<button class='btn btn-danger' type="button" onclick='confirm_delete("table");'><i class="bi bi-trash3-fill"></i></button>
</div>
<form id="form_delete_table" action="/table/delete" method="post">
<input value="${tblid}" name="tblid" hidden />
</form>`;
edit_mode = true;
document.getElementById('pencil_button_edit_tname').hidden = true;
} else {
document.getElementById('pencil_button_edit_tname').hidden = false;
span.innerHTML = `${tblname}`;
edit_mode = false;
}
}
function edit_tname() {
document.getElementById('form_edit_table').submit();
toggle_edit_tname();
}

View File

@ -23,7 +23,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">New table</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_create_table).trigger('reset');"></button>
</div>
<div class="modal-body">
<form id="form_create_table" method="post" action="/table/create">
@ -77,7 +77,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Import new table</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_import_table').trigger('reset');"></button>
</div>
<div class="modal-body">
<form action="/table/import" method="post" id="form_import_table">

View File

@ -64,5 +64,51 @@
{% endfor %}
</div>
<hr>
{% if sharednms |length > 1 %}
<div class="accordion" id="shared_tables">
{% for tname in sharednms %}
{% set clms = sharedcols[loop.index0] %}
{% set rws = sharedrows[loop.index0] %}
{% set tid = sharedtblids[loop.index0] %}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#acc_shared_{{ tid }}" aria-expanded="false" aria-controls="acc_shared_{{ tid }}">
{{ tname }}
</button>
</h2>
<div id="acc_shared_{{ tid }}" class="accordion-collapse collapse" data-bs-parent="#shared_tables">
<div class="accordion-body">
<div class="row justify-content-between">
<div class="col-auto"><h3>{{ tname }}</h3></div><div class="col-auto"><a class="btn btn-primary" href="/table/{{ tid }}">Open</a></div>
</div>
<!-- Preview -->
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
{% for clm in clms %}
<th>{{ clm }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rws %}
<tr>
<td>{{ loop.index }}</td>
{% for clm in clms %}
<td>{{ row[loop.index0] }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock body %}

View File

@ -25,25 +25,30 @@
<!-- Table header and editing -->
<div class="row justify-content-between">
<div class="col-auto">
<div class="col-2 me-0 pe-0 pt-0 mt-0">
<h1>
<div class='input-group'>
<span id="tname">{{ tblname }}</span>
<button class="btn" type="button" onclick="toggle_edit_tname();" id="pencil_button_edit_tname">
<i class="bi bi-pencil"></i>
</button>
</div>
<div class='input-group mt-0'>
<span id="tname">{{ tblname }}</span>
{% block edit_tname %}
{% endblock edit_tname %}
</div>
</h1>
</div>
<div class="col-1 align-self-start align-content-start ms-0 ps-0 mt-0 pt-0">
{% block share %}
{% endblock share %}
</div>
<div class="col-6">
</div>
<!-- Search bar -->
<div class="col-auto">
<div class="btn-toolbar mt-2" role="toolbar">
<div class="btn-group me-2">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#create_entry_modal">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<div class="col-3 align-self-end align-content-end pt-0 mt-0">
<div class="btn-toolbar mt-0 pt-0" role="toolbar">
{% block new_entry %}
{% endblock new_entry %}
<form method="get" action="/table/{{ tblid }}">
<div class="input-group">
<div class="input-group-text dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
@ -81,7 +86,8 @@
</div>
<div class="col-auto">
<div class="btn-toolbar">
<button class="btn" data-bs-toggle="modal" data-bs-target="#create_column_modal" type="button"><i class="bi bi-bookmark-plus"></i></button>
{% block new_column %}
{% endblock new_column %}
<form method="get" action="/table/{{ tblid }}">
<input value="0" name="sort_field" hidden />
<input value="{% if sort_field == 0 %}{{ (sort_dir + 1) % 2}}{% else %}0{% endif %}" name="sort_dir" hidden />
@ -105,7 +111,7 @@
</div>
<div class="col-auto">
<div class="btn-toolbar">
<button class="btn p-0 me-2" type="button" data-bs-toggle="modal" data-bs-target="#edit_column_modal" onclick="edit_column({{ loop.index0 }});"><i class="bi bi-pencil"></i></button>
<button class="btn p-0 me-2 edit_column_btn" type="button" data-bs-toggle="modal" data-bs-target="#edit_column_modal" onclick="edit_column({{ loop.index0 }});" hidden><i class="bi bi-pencil"></i></button>
<form method="GET" action="/table/{{ tblid }}">
<input value="{{ loop.index }}" name="sort_field" hidden />
<input value="{% if sort_field == loop.index %}{{ (sort_dir + 1) % 2}}{% else %}0{% endif %}" name="sort_dir" hidden />
@ -136,20 +142,5 @@
</tbody>
</table>
{% endblock body %}
{% block modals %}
<!-- Table specific modals -->
{% include "table_modals" %}
{% endblock modals %}
{% block script %}
<!-- table specific values -->
<script type="text/javascript">
const column_names = [{% for col in column_names %} '{{ col }}', {% endfor %}];
const column_types = [{% for col in column_types %} {{ col }}, {% endfor %}];
const tblid = {{ tblid }};
const tblname = '{{ tblname }}';
</script>
<!-- Table specific functionality -->
<script type="text/javascript" src="/js/table.js"></script>
{% endblock script %}

View File

@ -0,0 +1,25 @@
{% extends "table_write" %}
{% block edit_tname %}
<button class="btn" type="button" onclick="toggle_edit_tname();" id="pencil_button_edit_tname">
<i class="bi bi-pencil"></i>
</button>
{% endblock edit_tname %}
{% block share %}
<div class="button-group mt-0">
<button class="btn btn-outline-dark mt-0" data-bs-toggle="modal" data-bs-target="#share_table_modal"><i class="bi bi-send-arrow-up"></i></button>
</div>
{% endblock share %}
{% block more_modals %}
{{ super() }}
<!-- Table owned specific modals -->
{% include "table_owned_modals" %}
{% endblock more_modals %}
{% block script %}
<!-- Table owned specific functionality -->
<script type="text/javascript" src="/js/table_owned.js"></script>
{% endblock script %}

View File

@ -0,0 +1,65 @@
{% block modal_share_table %}
<!-- Share table -->
<div class="modal fade" id="share_table_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">
Share table
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$( '#form_share_table' ).trigger('reset');"></button>
</div>
<div class="modal-body">
<form action="/table/share" method="post" id="form_share_table" onreset="$('.2benabled').prop('disabled', false);">
<input name="tblid" value="{{ tblid }}" hidden>
{% for user in shared %}
<div class="row py-1">
<div class="col-auto">
<input value="{{ user.username }}" readonly id="form_share_table_user_{{ user.id }}" class="form-control 2benabled" />
<input value="{{ user.id }}" name="sharees" hidden />
</div>
<div class="col-auto form-check form-check-inline pt-2">
<label for="form_share_table_ro_{{ user.id }}" class="form-check-label"> Read only </label>
<input id="form_share_table_ro_{{ user.id }}" type="checkbox" class="form-check-input 2benabled" {% if user.readonly %}checked{% endif %} onchange="$('#form_share_table_action_ro_{{ user.id }}').val($(this).is(':checked'));" />
</div>
<div class="col-auto">
<input id="form_share_table_action_ro_{{ user.id }}" name="readonly" value="{{ user.readonly }}" hidden>
<input id="form_share_table_action_del_{{ user.id }}" name="delete" value="false" hidden>
<button id="form_share_table_delete_button_{{ user.id }}" class="btn btn-outline-danger 2benabled" type="button" onclick="$('#form_share_table_action_del_{{ user.id }}').val(true); $('#form_share_table_ro_{{ user.id }}').prop('disabled', true); $('#form_share_table_user_{{ user.id }}').prop('disabled', true); $('#form_share_table_delete_button_{{ user.id }}').prop('disabled', true);"><i class="bi bi-trash3"></i></button>
</div>
</div>
{% endfor %}
<div class="row py-1">
<div class="col-auto">
<input id="form_share_table_new_user_input" placeholder="User" class="form-control" onchange="query_users($(this).val());" autofocus autocomplete="off" />
<input id="form_share_table_new_user_id" name="new_user" hidden>
<div class="card text-bg-light mt-2">
<ul class="list-group list-group-flush" id="form_share_table_select_new_user">
<!-- users to select from -->
</ul>
</div>
</div>
<div class="col-auto form-check form-check-inline pt-2">
<label for="form_share_table_new_ro">Read-only</label>
<input name="readonly" type="checkbox" class="form-check-input" checked />
</div>
<div class="col-auto">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<div class="row justify-content-betwen">
<div class="col-auto">
</div>
<div class="col-auto">
<button class="btn btn-secondary" data-bs-dismiss="modal" onclick="$('#form_share_table').trigger('reset');"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" onclick="$('#form_share_table').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock modal_share_table %}

View File

@ -0,0 +1 @@
{% extends "table" %}

View File

@ -0,0 +1,38 @@
{% extends "table" %}
{% block new_entry %}
<div class="btn-group me-2 mt-0">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#create_entry_modal">
<i class="bi bi-plus-lg"></i>
</button>
</div>
{% endblock new_entry %}
{% block new_column %}
<button class="btn" data-bs-toggle="modal" data-bs-target="#create_column_modal" type="button"><i class="bi bi-bookmark-plus"></i></button>
{% endblock new_column %}
{% block edit_column %}
{% endblock edit_column %}
{% block modals %}
<!-- Table write specific modals -->
{% include "table_write_modals" %}
{% endblock modals %}
{% block script %}
<!-- table specific values -->
<script type="text/javascript">
const column_names = [{% for col in column_names %} '{{ col }}', {% endfor %}];
const column_types = [{% for col in column_types %} {{ col }}, {% endfor %}];
const tblid = {{ tblid }};
const tblname = '{{ tblname }}';
</script>
<!-- Table write specific functionality -->
<script type="text/javascript" src="/js/table_write.js"></script>
{% endblock script %}

View File

@ -23,7 +23,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Add entry</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_create_entry').reset();"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_create_entry').trigger('reset');"></button>
</div>
<div class="modal-body">
<h5>Cell values:</h5>
@ -49,8 +49,8 @@
<div class="col-auto">
</div>
<div class="col-auto">
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_create_entry').reset();"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="document.getElementById('form_create_entry').submit().reset();"><i class="bi bi-check-lg"></i></button>
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_create_entry').trigger('reset');"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="$('#form_create_entry').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
@ -66,7 +66,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5"><div class="row"><div class="col-auto">Row <span id="modal_caller"></span></div><div class="col-auto"><i class="bi bi-pencil-fill"></i></div></div></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_edit_entry').reset();"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_edit_entry').trigger('reset');"></button>
</div>
<div class="modal-body">
<form action="/row/edit" method="post" id="form_edit_entry">
@ -96,8 +96,8 @@
</form>
</div>
<div class="col-auto">
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_edit_entry').reset();"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_edit_entry');" class="bi bi-check-lg"></i></button>
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_edit_entry').trigger('reset');"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="$('#form_edit_entry').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
@ -108,7 +108,7 @@
{% block modal_new_column %}
<!-- Add column -->
<div class="modal fade" id="create_column_modal" tabindex="-1" aria-hidden="true" onclick="document.getElementById('form_create_column').reset();">
<div class="modal fade" id="create_column_modal" tabindex="-1" aria-hidden="true" onclick="$('#form_create_column').trigger('reset');">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -137,8 +137,8 @@
</div>
<div class="modal-footer">
<div class="col-auto">
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_create_column').reset();"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_create_column');"><i class="bi bi-check-lg"></i></button>
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$'#form_create_column').trigger('reset');"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="$('#form_create_column').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>
@ -153,7 +153,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5"><div class="row"><div class="col-auto"><span id="modal_caller"></span></div><div class="col-auto"><i class="bi bi-pencil-fill"></i></div></div></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="$('#form_edit_column').trigger('reset');"></button>
</div>
<div class="modal-body">
<form action="/column/edit" method="post" id="form_edit_column">
@ -186,8 +186,8 @@
</form>
</div>
<div class="col-auto">
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="document.getElementById('form_edit_column').reset();"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="submit_and_reset('form_edit_column');"><i class="bi bi-check-lg"></i></button>
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button" onclick="$('#form_edit_column').trigger('reset');"><i class="bi bi-x"></i></button>
<button class="btn btn-primary" type="button" onclick="$('#form_edit_column').trigger('submit').trigger('reset');"><i class="bi bi-check-lg"></i></button>
</div>
</div>
</div>