Compare commits

..

No commits in common. "main" and "v0.1" have entirely different histories.
main ... v0.1

34 changed files with 283 additions and 953 deletions

2
.gitignore vendored
View File

@ -188,5 +188,3 @@ Rocket.toml
**/*~
**/.#*
**/#*#
update.sh

View File

View File

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

View File

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

View File

@ -1,13 +0,0 @@
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

@ -21,12 +21,26 @@ use crate::jrcolumns;
use crate::jrentries;
use models::{ Jrcell, NewJrcell };
use schema::jrcells::dsl::*;
use schema::jrcells::dsl::{jrcells, id, jrentry_id, jrcolumn_id, cell_value};
use diesel::prelude::*;
use diesel::mysql::MysqlConnection;
pub fn create_jrcell_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, idintbl: i32, value: String) -> Result<usize, diesel::result::Error> {
let ntrid = jrentries::get_jrentry_id(conn, tblid, rowpos);
if ntrid.is_err() {
return Err(ntrid.err().unwrap());
}
let ntrid = ntrid.unwrap();
let clmid = jrcolumns::get_clmid_relative(conn, tblid, idintbl);
if clmid.is_err() {
return Err(clmid.err().unwrap());
}
let clmid = clmid.unwrap();
create_jrcell(conn, ntrid, clmid, &value)
}
pub fn change_jrcell_value_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, idintbl: i32, new_value: &String) -> Result<usize, diesel::result::Error> {
let ntrid = jrentries::get_jrentry_id(conn, tblid, rowpos);
if ntrid.is_err() {
@ -41,6 +55,20 @@ pub fn change_jrcell_value_relative(conn: &mut MysqlConnection, tblid: i32, rowp
change_jrcell_value(conn, ntrid, clmid, new_value)
}
pub fn delete_jrcell_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, idintbl: i32) -> Result<usize, diesel::result::Error> {
let ntrid = jrentries::get_jrentry_id(conn, tblid, rowpos);
if ntrid.is_err() {
return Err(ntrid.err().unwrap());
}
let ntrid = ntrid.unwrap();
let clmid = jrcolumns::get_clmid_relative(conn, tblid, idintbl);
if clmid.is_err() {
return Err(clmid.err().unwrap());
}
let clmid = clmid.unwrap();
delete_jrcell(conn, ntrid, clmid)
}
pub fn create_jrcell(conn: &mut MysqlConnection, entryid: i32, columnid: i32, value: &String) -> Result<usize, diesel::result::Error> {
use self::schema::jrentries::dsl::jrentries;
use self::schema::jrcolumns::dsl::jrcolumns;
@ -73,6 +101,12 @@ pub fn change_jrcell_value(conn: &mut MysqlConnection, entryid: i32, columnid: i
.execute(conn)
}
pub fn delete_jrcell(conn: &mut MysqlConnection, entryid: i32, columnid: i32) -> Result<usize, diesel::result::Error> {
diesel::delete(jrcells.filter(jrentry_id.eq(entryid)).filter(jrcolumn_id.eq(columnid)))
.execute(conn)
}
pub fn get_entry_cells(conn: &mut MysqlConnection, entryid: i32) -> Result<Vec<Jrcell>, diesel::result::Error> {
jrcells
.filter(jrentry_id.eq(entryid))
@ -80,3 +114,9 @@ pub fn get_entry_cells(conn: &mut MysqlConnection, entryid: i32) -> Result<Vec<J
.load(conn)
}
pub fn get_entry_cell_ids(conn: &mut MysqlConnection, entryid: i32) -> Result<Vec<i32>, diesel::result::Error> {
jrcells
.filter(jrentry_id.eq(entryid))
.select(id)
.load(conn)
}

View File

@ -124,10 +124,7 @@ pub fn delete_jrentry(conn: &mut MysqlConnection, entryid: i32) -> Result<usize,
if nr.is_err() {
return nr;
}
let mr = move_jrentry(conn, tblid, rp, nr.unwrap() as i32);
if mr.is_err() {
return mr;
}
move_jrentry(conn, tblid, rp, nr.unwrap() as i32);
diesel::delete(jrentries.find(entryid)).execute(conn)
}

View File

@ -60,6 +60,12 @@ pub fn delete_jrtable(conn: &mut MysqlConnection, tblid: i32) -> Result<usize, d
.execute(conn)
}
pub fn get_all_tables(conn: &mut MysqlConnection) -> Result<Vec<Jrtable>, diesel::result::Error> {
jrtables
.select(Jrtable::as_select())
.load(conn)
}
pub fn get_tbl(conn: &mut MysqlConnection, tblid: i32) -> Result<Jrtable, diesel::result::Error> {
jrtables
.find(tblid)

View File

@ -12,13 +12,10 @@
* 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/
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Library providing a spreadsheet-like table system.
//! Adapter to a Mysql DB.
//! This file exposes public methods and datastructures to interface the system.
//! Database API using the table style system provided by this crate
mod models;
mod schema;
mod jrtables;
@ -26,7 +23,6 @@ mod jrcolumns;
mod jrentries;
mod jrcells;
mod users;
mod shares;
use dotenvy::dotenv;
use std::env;
@ -39,12 +35,9 @@ 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, Debug)]
#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow)]
#[repr(i32)]
pub enum FIELDTYPE {
TEXT = 0,
@ -70,14 +63,13 @@ impl From<isize> for FIELDTYPE {
fn from(value: isize) -> Self {
match value {
1 => FIELDTYPE::NUMBER,
_ => FIELDTYPE::TEXT,
x => FIELDTYPE::TEXT,
}
}
}
/// 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,
@ -112,14 +104,6 @@ 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 {
@ -141,21 +125,14 @@ 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;
@ -166,6 +143,11 @@ fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{
clmn_nms.push(clmn.name.clone());
}
let clmn_nms = clmn_nms;
let mut clmn_ids = Vec::new();
for clmn in &clmns {
clmn_ids.push(clmn.id);
}
let clmn_ids = clmn_ids;
let mut clmn_tps = Vec::new();
for clmn in &clmns {
clmn_tps.push(clmn.column_type);
@ -190,26 +172,9 @@ fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{
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 }, tbl.owner_id))
Some(Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows })
}
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.
/// sort_field is the index of the column to sort by.
/// 0 is the position of the row in the table.
/// then 1-indexed column index
pub fn sort_table(tbl: Tbl, sort_field: usize, sort_dir: u8) -> Tbl {
let mut rows = tbl.rows;
if sort_field == 0 {
@ -231,12 +196,8 @@ pub fn sort_table(tbl: Tbl, sort_field: usize, sort_dir: u8) -> Tbl {
Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: rows.clone() }
}
/// Take a Tbl object, find all rows matching the search queries and return a new Tbl object containing only those.
/// search_fields contains a Vec of (1-indexed) column positions to be included in the search
/// search_value is a string to search for in the search_fields.
/// Returned Tbl is not sorted and should be according to user preferences before being displayed.
pub fn search_table(tbl: Tbl, search_fields: Vec<i32>, search_value: String) -> Tbl {
let rows = tbl.rows;
let mut rows = tbl.rows;
let mut field_sets = HashSet::new();
for field in search_fields {
for row in &rows {
@ -248,7 +209,7 @@ pub fn search_table(tbl: Tbl, search_fields: Vec<i32>, search_value: String) ->
Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: Vec::from_iter(field_sets) }
}
/// For a Vec a database ids of tables, return a Vec of their names in the same order.
pub fn get_tblnames(conn: &mut MysqlConnection, tblids: Vec<i32>) -> Option<Vec<String>> {
let mut tblnames = Vec::new();
for tblid in tblids {
@ -347,9 +308,6 @@ pub fn register_or_login(conn: &mut MysqlConnection, uname: String, mail: String
Some(User { uid: uid, uname: uname, email: mail })
}
/// Take a user id (database representation, not username) and delete it from the database.
/// All its tables are cascadingly deleted.
/// Return Some(true) if successful, None otherwise
pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Option<bool> {
if users::delete_user(conn, uid).is_err() {
return None;
@ -357,9 +315,6 @@ pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Option<bool> {
Some(true)
}
/// For a given id of a table and a vec of string values, create a new jrentry (table row) and all its jrcells (table cells) filled with the string values.
/// Only works if uid corresponds to the user id of the table's owner.
/// Return the id of the newly created row if successful, None otherwise.
pub fn add_row(conn: &mut MysqlConnection, tblid: i32, values: Vec<String>, uid: i32) -> Option<i32> {
let owner = jrtables::get_owner_id(conn, tblid);
let nrows = jrtables::get_nrows(conn, tblid);
@ -388,8 +343,6 @@ pub fn add_row(conn: &mut MysqlConnection, tblid: i32, values: Vec<String>, uid:
Some(entryid)
}
/// Delete a jrentry (table row) identified by the id of its table and the current position wihthin it
/// Return Some(true) if successful, None otherwise
pub fn delete_row(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() || owner.unwrap() != uid || jrentries::delete_jrentry_relative(conn, tblid, row_pos).is_err() {
@ -398,9 +351,6 @@ pub fn delete_row(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, uid: i32
Some(true)
}
/// Change a jrentry's (table row's) current position within its table, given the table's id, the current position and the new position it should be moved to.
/// Only works if uid equals to the owner's user id.
/// Returns Some(true) if successful, None otherwise.
pub fn move_row(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newpos: i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() || owner.unwrap() != uid || jrentries::move_jrentry(conn, tblid, rowpos, newpos).is_err() {
@ -409,9 +359,6 @@ pub fn move_row(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newpos: i32
Some(true)
}
/// Update a jrentry (table row) with a new set of string values, given those new values and the row identified by its table and its position within the table.
/// Only works if uid equals to the table's owner's user id.
/// Returns Some(true) if successful, None otherwise.
pub fn edit_row(conn: &mut MysqlConnection, tblid: i32, cells: Vec<String>, row_pos: i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() || owner.unwrap() != uid {
@ -425,9 +372,7 @@ pub fn edit_row(conn: &mut MysqlConnection, tblid: i32, cells: Vec<String>, row_
Some(true)
}
/// Edit the content of a single jrcell (table cell), given its table id, the position of its row and column within the table and the new value.
/// Only works if uid equals to the table's owner's user id.
/// Returns Some(true) if successful, None otherwise.
pub fn edit_cell(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, column_pos: i32, new_value: &String, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
@ -439,9 +384,6 @@ pub fn edit_cell(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, column_po
Some(true)
}
/// Change a jrcolumn's (table column's) current position within its table, given the table's id, the current position and the new position it should be moved to.
/// Only works if uid equals to the owner's user id.
/// Returns Some(true) if successful, None otherwise.
pub fn move_column(conn: &mut MysqlConnection, tblid:i32, column_pos: i32, new_column_pos: i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
@ -453,10 +395,6 @@ pub fn move_column(conn: &mut MysqlConnection, tblid:i32, column_pos: i32, new_c
Some(true)
}
/// Add a new jrcolumn (table column) to an existing table.
/// Required are the table's id, the column's name, the column's FIELDTYPE and the id of the logged in user.
/// Only works if uid equals to the table's owner's user id.
/// Returns the id of the newly created column if successful, None otherwise.
pub fn add_column(conn: &mut MysqlConnection, tblid: i32, name: String, clmtype: FIELDTYPE, uid: i32) -> Option<i32> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
@ -472,9 +410,6 @@ pub fn add_column(conn: &mut MysqlConnection, tblid: i32, name: String, clmtype:
Some(clmid.unwrap())
}
/// Delete a column identified by its table's id and its position within the table.
/// Only works if uid equals to the table's owner's id.
/// Returns Some(true) if successful, None otherwise.
pub fn delete_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
@ -486,9 +421,6 @@ pub fn delete_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, ui
Some(true)
}
/// Update a jrcolumn (table column) with a given new name, and new type. The column is identified by its table's id and its position within it.
/// Only works if uid equals to the table's owner's id.
/// Returns Some(true) if successful, None otherwise.
pub fn edit_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_name: &String, new_type: FIELDTYPE, uid: i32) -> Option<bool> {
let owner = jrtables::get_owner_id(conn, tblid);
if owner.is_err() ||
@ -499,83 +431,3 @@ 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, shares};
use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users};
#[derive(Queryable, Selectable, Identifiable, Associations)]
#[diesel(table_name = crate::schema::jrtables)]
@ -114,23 +114,3 @@ 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,3 +1,20 @@
/* 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! {
@ -38,15 +55,6 @@ diesel::table! {
}
}
diesel::table! {
shares (id) {
id -> Integer,
sharee_id -> Integer,
tblid -> Integer,
readonly -> Bool,
}
}
diesel::table! {
users (id) {
id -> Integer,
@ -62,14 +70,11 @@ 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,
);

View File

@ -1,53 +0,0 @@
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::{ User, NewUser };
use models::NewUser;
use diesel::prelude::*;
use diesel::mysql::MysqlConnection;
@ -58,11 +58,17 @@ 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> {
pub fn get_email(conn: &mut MysqlConnection, uid: i32) -> Result<String, diesel::result::Error> {
users
.filter(username.like(format!("{query}%")))
.select(User::as_select())
.load::<User>(conn)
.find(uid)
.select(email)
.first::<String>(conn)
}
pub fn get_user_tables(conn: &mut MysqlConnection, uid: i32) -> Result<Vec<i32>, diesel::result::Error> {
use schema::jrtables::dsl::{jrtables, id as tblid, owner_id};
jrtables
.filter(owner_id.eq(uid))
.select(tblid)
.load::<i32>(conn)
}

View File

@ -15,8 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Module responsible for handling user authentication
use crate::Db;
use rocket::response::Redirect;
@ -25,12 +23,11 @@ use rocket::http::{Status, Cookie, CookieJar, SameSite};
use inventur_db;
use rocket_oauth2::{OAuth2, TokenResponse};
use reqwest::Client;
use rocket::serde::Deserialize;
use rocket::serde::{Deserialize, json::Json};
/// OAuth provider
pub struct RanderathIdentity;
/// User attempting to log in. Username and email provided by the oauth provider.
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct UnAuthUser {
@ -38,15 +35,12 @@ struct UnAuthUser {
email: String,
}
/// User currently logged in and authenticated.
pub struct AuthUser {
pub uid: i32,
pub uname: String,
pub email: String,
}
/// Using the client browser's private cookies, check if the user is logged in.
/// If yes, calculate the corresponding AuthUser, else Forward to Unauthorized.
#[rocket::async_trait]
impl<'r> request::FromRequest<'r> for AuthUser {
type Error = ();
@ -69,8 +63,6 @@ impl<'r> request::FromRequest<'r> for AuthUser {
}
}
/// Utility function invoked when trying to log in to fetch user data from the database, given an oauth access token.
/// Fetch user info from oauth provider using the access token, calculate the UnAuthUser from the Json response, query the database and return the user if it exists or create it if it doesn't.
pub async fn login_or_register(conn: Db, access_token: &str) -> Option<inventur_db::User> {
let ui = Client::new()
.get("https://ldap.randerath.eu/realms/master/protocol/openid-connect/userinfo")
@ -93,29 +85,20 @@ pub async fn login_or_register(conn: Db, access_token: &str) -> Option<inventur_
}
/// Unauthorized requests are sent to the oauth provider in order for the user to authenticate.
#[catch(401)]
pub async fn redirect_to_login_401() -> Redirect {
Redirect::to(uri!(oauth_login))
}
/// Unauthorized requests are sent to the oauth provider in order for the user to authenticate.
#[catch(403)]
pub async fn redirect_to_login_403() -> Redirect {
pub async fn redirect_to_login() -> Redirect {
Redirect::to(uri!(oauth_login()))
}
/// Redirect to oauth provider, fetching the correct link.
#[get("/login")]
pub fn oauth_login(oauth2: OAuth2<RanderathIdentity>, cookies: &CookieJar<'_>) -> Redirect {
oauth2.get_redirect(cookies, &["openid"]).unwrap()
}
/// Callback called by the oauth provider
/// Process TokenResponse, set login cookies and log the user in.
#[get("/auth")]
pub async fn oauth_callback(conn: Db, token: TokenResponse<RanderathIdentity>, cookies: &CookieJar<'_>) -> Result<Redirect, Status> {
let at = token.access_token().to_string();
let tv = token.as_value();
cookies.add_private(
Cookie::build(("token", at.to_string()))
.same_site(SameSite::Lax)

View File

@ -15,53 +15,37 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Rocket app to set up a platform of cloud-stored tables for keeping track of data records.
//! Basically inventur tries to combine the ease of use of spreadsheets with the entry-based system of databases.
//! Oauth2 is used as the authentication system, diesel mysql and the subcrate inventur_db are used for database handling.
#[macro_use] extern crate rocket;
/// Views, utility methods and data structures related to user authentication
mod auth;
/// Views, utility mehtods and data structures, related to displaying and modifying tables (everything the user sees under /table).
mod table;
/// Data structure representing an authenticated, logged in user.
use auth::AuthUser;
use rocket::fs::{FileServer, relative};
use rocket_dyn_templates::{Template, context};
use rocket::response::{Flash, Redirect};
use rocket::request;
use rocket::response::Redirect;
use rocket::http::{Status, Cookie, CookieJar, SameSite};
use inventur_db;
use rocket_sync_db_pools::{database, diesel};
use rocket_oauth2::OAuth2;
use std::env;
use dotenvy::dotenv;
use rocket::Config;
use rocket::figment::providers::Env;
use rocket::http::{CookieJar, Status};
use rocket::form::Form;
use rocket::serde::json::Json;
use rocket::figment::providers::{Toml, Env, Format};
use table::forms::{SearchString, UserQueryResult};
/// Database connection using diesel and rocket_sync_db_pools
#[database("inventur")]
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) -> Option<Template> {
async fn home(conn: Db, user: AuthUser) -> 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;
if tbl.is_none() {
return None;
}
let tbl = tbl.unwrap();
let tbl = conn.run(move |c| inventur_db::get_table(c, tblid, uid)).await.unwrap();
cols.push(tbl.column_names.clone());
let mut rws : Vec<Vec<String>> = Vec::new();
for row in tbl.rows {
@ -69,98 +53,26 @@ async fn home(conn: Db, user: AuthUser) -> Option<Template> {
}
rows.push(rws.clone());
}
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,
}
)
)
}
#[get("/logout")]
async fn logout(_user: AuthUser, cookies: &CookieJar<'_>) -> Flash<Redirect> {
for cookie in cookies.iter() {
cookies.remove_private(cookie.clone());
}
Flash::success(Redirect::to("https://ldap.randerath.eu/realms/master/protocol/openid-connect/logout"), "Goodbye!")
}
/// If no user is authenticated, redirect the user to authenticate with the oauth identity provider.
#[get("/", rank=2)]
async fn login_home() -> Redirect {
Redirect::to(uri!(auth::oauth_login()))
}
/// Serve the favicon as if it were in /favicon.ico
#[get("/favicon.ico")]
async fn favicon() -> Redirect {
Redirect::to(uri!("/img/favicon.ico"))
}
#[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 ...
/// ... home page and login under /
/// ... display of a table, and the manipulation of the table object under /table
/// ... manipulation of table rows under /row
/// ... manipulation of columns under /column
/// ... requests not logged in to the oauth provider
/// Setup a fileserver to serve static files from the static directory in the file type's directory
#[launch]
async fn rocket() -> _ {
dotenv().ok();
@ -168,18 +80,20 @@ async fn rocket() -> _ {
let cfg = Config::figment()
.merge(Env::prefixed("ROCKET_"));
rocket::custom(cfg)
.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, 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("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home, favicon])
.mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create_table, table::import_table, table::delete_table])
.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("/column", routes![table::delete_column, table::edit_column])
.register("/", catchers![auth::redirect_to_login])
.mount("/img", FileServer::from(relative!("static/img")))
.mount("/css", FileServer::from(relative!("static/css")))
.mount("/js", FileServer::from(relative!("static/js")))
.mount("/txt", FileServer::from(relative!("static/txt")))
}

View File

@ -16,21 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Module responsible for handling display, query and modification of tables, rows, columns and cells.
/// Everything related to displaying the tables
pub mod table_view;
/// Everything related to manipulating table objects (as opposed to its member).
pub mod table_manipulate_table;
/// Everything related to manipulating table columns and their types.
pub mod table_manipulate_column;
/// Everything related to manipulating table rows and cells.
pub mod table_manipulate_entry;
/// Structs used to handle form data for manipulating the database.
pub mod forms;
pub use self::table_view::*;
pub use self::table_manipulate_table::*;
pub use self::table_manipulate_entry::*;
pub use self::table_manipulate_column::*;
pub use self::forms::*;

View File

@ -16,10 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Submodule holding structs relevant to handle form data.
use rocket::form::Form;
use rocket::serde::{Serialize, Deserialize};
//use inventur_db::ShareCard;
#[derive(FromForm)]
pub struct DeleteColumn {
@ -61,7 +59,6 @@ pub struct DeleteEntry {
pub row_pos: i32,
}
/// Edit a table's name
#[derive(FromForm)]
pub struct EditTname {
pub tblid: i32,
@ -88,38 +85,8 @@ pub struct DeleteTable {
pub tblid: i32,
}
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[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,
pub struct JRows {
pub rows: Vec<Vec<String>>,
}

View File

@ -16,9 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Submodule responsible for manipulating and creating a table's column.
//! For all methods in this module, the authenticated user must be the table's owner.
use crate::auth;
use crate::Db;
use crate::table::forms;
@ -36,7 +33,6 @@ use auth::AuthUser;
use self::forms::{NewColumn, EditColumn, DeleteColumn};
use self::table_view::rocket_uri_macro_table_sec;
/// Add a new column to an existing table
#[post("/create", data="<data>")]
pub async fn create_column(conn: Db, data: Form<NewColumn>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -48,7 +44,6 @@ pub async fn create_column(conn: Db, data: Form<NewColumn>, user: AuthUser) -> R
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Edit a column (its name and type)
#[post("/edit", data="<data>")]
pub async fn edit_column(conn: Db, data: Form<EditColumn>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -61,7 +56,6 @@ pub async fn edit_column(conn: Db, data: Form<EditColumn>, user: AuthUser) -> Re
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Delete a column
#[post("/delete", data="<data>")]
pub async fn delete_column(conn: Db, data: Form<DeleteColumn>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;

View File

@ -16,9 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Submodule responsible for manipulating and creating rows and cells to existing tables
//! For all methods in this module, the authenticated user has to be the table's owner
use crate::auth;
use crate::Db;
use crate::table::forms;
@ -35,7 +32,6 @@ use auth::AuthUser;
use self::forms::{NewEntry, EditEntry, DeleteEntry};
use self::table_view::rocket_uri_macro_table_sec;
/// Create a new a new row to an existing table, filling all its columns with the supplied values.
#[post("/new", data="<data>")]
pub async fn new_entry(conn: Db, data: Form<NewEntry>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -47,7 +43,6 @@ pub async fn new_entry(conn: Db, data: Form<NewEntry>, user: AuthUser) -> Result
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Edit a row's cell contents, the row being identified by its table and its position within the table.
#[post("/edit", data="<data>")]
pub async fn edit_entry(conn: Db, data: Form<EditEntry>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -60,7 +55,6 @@ pub async fn edit_entry(conn: Db, data: Form<EditEntry>, user: AuthUser) -> Resu
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Delete a row identified by its table and its position within the table.
#[post("/delete", data="<data>")]
pub async fn delete_entry(conn: Db, data: Form<DeleteEntry>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;

View File

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Submodule responsible for manipulating or creating table objects (not individual rows, columns or entries).
use crate::auth;
use crate::Db;
use crate::table::forms;
@ -33,11 +31,9 @@ use rocket::serde::json::Json;
use inventur_db::FIELDTYPE;
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname, EditShare};
use self::forms::{NewTable, DeleteTable, ImportTable, EditTname};
use self::table_view::rocket_uri_macro_table_sec;
/// Delete a table.
/// The authenticated user must be the table's owner (ensured by inventur_db).
#[post("/delete", data="<data>")]
pub async fn delete_table(conn: Db, data: Form<DeleteTable>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -48,7 +44,6 @@ pub async fn delete_table(conn: Db, data: Form<DeleteTable>, user: AuthUser) ->
Ok(Redirect::to(uri!("/")))
}
/// Create a new table with the given fields and name, make the authenticated user the new table's owner.
#[post("/create", data="<data>")]
pub async fn create_table(conn: Db, data: Form<NewTable>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -59,8 +54,6 @@ pub async fn create_table(conn: Db, data: Form<NewTable>, user: AuthUser) -> Res
Ok(Redirect::to(uri!("/table", table_sec(tblid.unwrap()))))
}
/// Create a new table including rows by importing a csv file.
/// Table's column types default to text.
#[post("/import", data="<data>")]
pub async fn import_table(conn: Db, data: Json<ImportTable>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -77,7 +70,6 @@ pub async fn import_table(conn: Db, data: Json<ImportTable>, user: AuthUser) ->
Ok(Redirect::to(uri!("/table", table_sec(tblid))))
}
/// Edit a table object (the table's name).
#[post("/name/edit", data="<data>")]
pub async fn edit_tname(conn: Db, data: Form<EditTname>, user: AuthUser) -> Result<Redirect, Status> {
let uid = user.uid;
@ -88,34 +80,4 @@ 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

@ -16,40 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Submodule responsible for handling fetching and displaying a table on the /table view.
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;
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 {
#[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();
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();
tbl = inventur_db::search_table(tbl, searchfields.clone(), searchvalue.clone());
table = inventur_db::search_table(table, searchfields.clone(), searchvalue.clone());
}else {
if search_value.is_none() {
searchvalue = String::new();
@ -57,7 +45,7 @@ async fn sort_and_search(tbl: Tbl, sort_dir: Option<u8>, sort_field: Option<usiz
searchvalue = search_value.unwrap();
}
if search_fields.is_none() {
searchfields = (0i32..(tbl.column_names.len() as i32)).collect();
searchfields = (0i32..(table.column_names.len() as i32)).collect();
} else {
searchfields = search_fields.unwrap();
@ -76,104 +64,35 @@ async fn sort_and_search(tbl: Tbl, sort_dir: Option<u8>, sort_field: Option<usiz
}else {
sortfield = sort_field.unwrap();
}
let table = inventur_db::sort_table(table, sortfield, sortdir);
let tbl = inventur_db::sort_table(tbl.clone(), 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: tbl.clone(), 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",
Template::render("table",
context!{
base_url: "/table",
search_value: passtable.searchvalue,
search_fields: passtable.searchfields,
sort_field: passtable.sortfield,
sort_dir: passtable.sortdir,
search_value: searchvalue,
search_fields: searchfields,
sort_field: sortfield,
sort_dir: sortdir,
tblid: tid,
tblname: passtable.tname,
tblname: 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,
column_names: column_names,
column_types: column_types,
rows: rows,
}
)
)
}
#[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_readonly",
context!{
base_url: "/table/readonly",
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,
}
)
)
}
/// View to redirect a post request handled to manipulate a table or its display representation back to the (new) table view.
/// Also uses table() but nulls all optional fields.
#[get("/<tid>", rank=2)]
pub async fn table_sec(tid: i32) -> Redirect {
pub async fn table_sec(conn: Db, tid: i32, user: AuthUser) -> Redirect {
let nus : Option<usize> = None;
let nu8 : Option<u8> = None;
let nvi32 : Option<Vec<i32>> = None;
@ -181,7 +100,6 @@ pub async fn table_sec(tid: i32) -> Redirect {
Redirect::to(uri!("/table", table(tid, nu8, nus, nvi32, ns)))
}
/// Utility function to get all a user's tables as their ids.
pub async fn get_tids(conn: &Db, uid: i32) -> (Vec<i32>, Vec<String>) {
let mut tids = conn.run(move |c| inventur_db::get_user_tblids(c, uid)).await;
let tnames;

View File

@ -24,3 +24,6 @@ footer {
width: 100%;
}
body {
width: 100%;
}

85
static/js/table.js Normal file
View File

@ -0,0 +1,85 @@
/* This file is part of inventur.
* inventur is a 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/>.
*/
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);
}
});
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];
document.getElementById('modal_caller').innerHTML = column_names[clmn_index];
document.getElementById('form_delete_column_idintbl').value = clmn_index + 1;
document.getElementById('form_edit_column_idintbl').value = clmn_index + 1;
}

View File

@ -1,71 +0,0 @@
document.addEventListener("DOMContentLoaded", 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

@ -1,51 +0,0 @@
/* This file is part of inventur.
* inventur is a 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/>.
*/
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');
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 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];
document.getElementById('modal_caller').innerHTML = column_names[clmn_index];
document.getElementById('form_delete_column_idintbl').value = clmn_index + 1;
document.getElementById('form_edit_column_idintbl').value = clmn_index + 1;
}

View File

@ -63,15 +63,6 @@
</a>
</li>
</ul>
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ username }}
</a>
<ul class="dropdown-menu dropdown-menu-end px-2">
<li class="dropdown-option"><a class="nav-link" href="https://ldap.randerath.eu/realms/master/account">Account</a></li>
<li class="dropdown-option"><a class="nav-link" href="/logout">Logout</a></li>
</ul>
</div>
</div>
</div>
</nav>
@ -79,8 +70,6 @@
<div id="content" class="mx-3">
{% block body %}{% endblock body %}
</div>
<!-- Footer -->
<div class="container mt-auto">
<footer class="d-flex flex-wrap border-top py-3 my-4 justify-content-between align-items-center">
<p class="col-md-4 mb-0 text-body-secondary">&copy; 2024 Johannes Randerath.</p>
@ -92,7 +81,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link text-body-secondary px-2" href="https://gitea.randerath.eu/johannes/Inventur/releases/tag/v0.1">
<a class="nav-link text-body-secondary px-2" href="https://gitea.randerath.eu/johannes/inventur">
Source
</a>
</li>

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" onclick="$('#form_create_table).trigger('reset');"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></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" onclick="$('#form_import_table').trigger('reset');"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="/table/import" method="post" id="form_import_table">

View File

@ -64,51 +64,5 @@
{% 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,30 +25,25 @@
<!-- Table header and editing -->
<div class="row justify-content-between">
<div class="col-auto me-0 pe-0 pt-0 mt-0">
<div class="col-auto">
<h1>
<div class='input-group mt-0'>
<div class='input-group'>
<span id="tname">{{ tblname }}</span>
{% block edit_tname %}
{% endblock edit_tname %}
<button class="btn" type="button" onclick="toggle_edit_tname();" id="pencil_button_edit_tname">
<i class="bi bi-pencil"></i>
</button>
</div>
</h1>
</div>
<div class="col-auto align-self-start align-content-start ms-0 ps-0 mt-0 pt-2">
{% block share %}
{% endblock share %}
</div>
<div class="col-auto">
</div>
<!-- Search bar -->
<div class="col-auto 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 %}
<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>
<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">
@ -86,8 +81,7 @@
</div>
<div class="col-auto">
<div class="btn-toolbar">
{% block new_column %}
{% endblock 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>
<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 />
@ -111,7 +105,7 @@
</div>
<div class="col-auto">
<div class="btn-toolbar">
<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>
<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>
<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 />
@ -142,5 +136,20 @@
</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

@ -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="$('#form_create_entry').trigger('reset');"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_create_entry').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="$('#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>
<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>
</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="$('#form_edit_entry').trigger('reset');"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="document.getElementById('form_edit_entry').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="$('#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>
<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>
</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="$('#form_create_column').trigger('reset');">
<div class="modal fade" id="create_column_modal" tabindex="-1" aria-hidden="true" onclick="document.getElementById('form_create_column').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="$'#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>
<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>
</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" onclick="$('#form_edit_column').trigger('reset');"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></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="$('#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>
<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>
</div>
</div>
</div>

View File

@ -1,26 +0,0 @@
{% 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 modals %}
{{ super() }}
<!-- Table owned specific modals -->
{% include "table_owned_modals" %}
{% endblock modals %}
{% block script %}
{{ super() }}
<!-- Table owned specific functionality -->
<script type="text/javascript" src="/js/table_owned.js"></script>
{% endblock script %}

View File

@ -1,65 +0,0 @@
{% 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

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

View File

@ -1,38 +0,0 @@
{% 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 %}