diff --git a/.env~ b/.env~ new file mode 100644 index 0000000..352a322 --- /dev/null +++ b/.env~ @@ -0,0 +1,3 @@ +R_DATABASES_URL=mysql://inventur:noiqC3OlFGVlRmU8KVPtyzVBLj4W9bbQhnu1zJ5cF75FH4JM2luhI2fvp5YXlTAuKr0Ayw2ZiUGfzortQ0GxqEQ5ICfEQgJtNfto@localhost:3306/inventur +ROCKET_SECRET_KEY=xT0JL6E1W4P+DNh0bRecNwsgtk0QfJ62t5zxe920s00= +ROCKET_CLIENT_SECRET=2vftRCu1WhaFezVoFgsNDlx66XOASBmX diff --git a/.gitignore b/.gitignore index 99e098c..ec4372c 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,6 @@ cython_debug/ # Added by cargo /target +.env +/private/ +Rocket.toml diff --git a/Cargo.toml b/Cargo.toml index acd920d..7af50cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,18 @@ version = "0.1.0" edition = "2021" [dependencies] -rocket = "0.5.1" +rocket = { version = "0.5.1", features = ["json", "tls"] } diesel = "2" +inventur_db = { path = "inventur_db/" } +rocket_oauth2 = "0.5" +reqwest = { version = "0.11", features = ["json"] } +dotenvy = "0.15" [dependencies.rocket_dyn_templates] version = "0.2.0" features = ["tera"] -[dependencies.rocket_db_pools] -version = "0.2.0" -features = ["diesel_mysql"] +[dependencies.rocket_sync_db_pools] +version = "0.1" +features = ["diesel_mysql_pool"] diff --git a/Rocket.toml b/Rocket.toml deleted file mode 100644 index f4639e6..0000000 --- a/Rocket.toml +++ /dev/null @@ -1,5 +0,0 @@ -[default] -template_dir = "templates" - -[default.databases.inventur] -url = "mysql://inventur:inventur@localhost/inventur?socket=/var/lib/mysql/mysql.sock" diff --git a/__Rocket.toml__ b/__Rocket.toml__ new file mode 100644 index 0000000..90d4a85 --- /dev/null +++ b/__Rocket.toml__ @@ -0,0 +1,20 @@ +[default] +template_dir = "templates" +secret_key = "${ROCKET_SECRET_KEY}" +address = "0.0.0.0" +port = 8001 +ident = "Randerath Inventur" + +[default.tls] +certs="private/cert.pem" +key="private/key.pem" + +#[default.databases.inventur] +#url = "${ROCKET_DATABASE_URL}" + +[default.oauth.oauth] +auth_uri = "https://ldap.randerath.eu/realms/master/protocol/openid-connect/auth" +token_uri = "https://ldap.randerath.eu/realms/master/protocol/openid-connect/token" +client_id = "randy_inventur" +client_secret = "${ROCKET_CLIENT_SECRET}" +redirect_uri = "https://inventur.rander.at/auth" diff --git a/inventur.service b/inventur.service new file mode 100644 index 0000000..aa61da1 --- /dev/null +++ b/inventur.service @@ -0,0 +1,14 @@ +[Unit] +Description=Table based database web app in rocket +After=keycloak + +[Service] +Type=Simple +WorkingDirectory=/usr/share/nginx/inventur/ +ExecStart=/usr/bin/inventur +User=nginx +Group=nginx +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/inventur_db/.gitignore b/inventur_db/.gitignore new file mode 100644 index 0000000..b67ff2c --- /dev/null +++ b/inventur_db/.gitignore @@ -0,0 +1,3 @@ +.env +target +bin/*~ diff --git a/inventur_db/Cargo.toml b/inventur_db/Cargo.toml new file mode 100644 index 0000000..1e8781e --- /dev/null +++ b/inventur_db/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "inventur_db" +version = "0.1.0" +edition = "2021" + +[dependencies] +diesel = { version = "2.2.0", features = ["mysql"] } +dotenvy = "0.15" diff --git a/inventur_db/README.md b/inventur_db/README.md new file mode 100644 index 0000000..8e69a99 --- /dev/null +++ b/inventur_db/README.md @@ -0,0 +1,4 @@ +Inventur DB +=========== + +This crate aids setting up a database system for a table-based information structuring system combining the intuitive use of spreadsheats with the data stability of databases. diff --git a/diesel.toml b/inventur_db/diesel.toml similarity index 79% rename from diesel.toml rename to inventur_db/diesel.toml index a04b848..9829ac5 100644 --- a/diesel.toml +++ b/inventur_db/diesel.toml @@ -6,4 +6,4 @@ file = "src/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] [migrations_directory] -dir = "/home/user/code/inventur/migrations" +dir = "/home/johannes/code/inventur/inventur_db/migrations" diff --git a/migrations/.keep b/inventur_db/migrations/.keep similarity index 100% rename from migrations/.keep rename to inventur_db/migrations/.keep diff --git a/inventur_db/migrations/2024-08-19-150533_create_table/#up.sql# b/inventur_db/migrations/2024-08-19-150533_create_table/#up.sql# new file mode 100644 index 0000000..19f9f41 --- /dev/null +++ b/inventur_db/migrations/2024-08-19-150533_create_table/#up.sql# @@ -0,0 +1,5 @@ + CREATE TABLE jrtables ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + num_fields INTEGER NOT NULL +); diff --git a/inventur_db/migrations/2024-08-19-150533_create_table/down.sql b/inventur_db/migrations/2024-08-19-150533_create_table/down.sql new file mode 100644 index 0000000..faf6537 --- /dev/null +++ b/inventur_db/migrations/2024-08-19-150533_create_table/down.sql @@ -0,0 +1 @@ +DROP TABLE jrtables; diff --git a/inventur_db/migrations/2024-08-19-150533_create_table/up.sql b/inventur_db/migrations/2024-08-19-150533_create_table/up.sql new file mode 100644 index 0000000..644b799 --- /dev/null +++ b/inventur_db/migrations/2024-08-19-150533_create_table/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE jrtables ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + num_fields INTEGER NOT NULL +); diff --git a/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql new file mode 100644 index 0000000..bbcd7de --- /dev/null +++ b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql @@ -0,0 +1 @@ +DROP TABLE jrcolumns; diff --git a/migrations/2024-08-09-133227_create_users/down.sql b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql~ similarity index 71% rename from migrations/2024-08-09-133227_create_users/down.sql rename to inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql~ index dc3714b..d9a93fe 100644 --- a/migrations/2024-08-09-133227_create_users/down.sql +++ b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/down.sql~ @@ -1,2 +1 @@ -- This file should undo anything in `up.sql` -DROP TABLE users; diff --git a/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql new file mode 100644 index 0000000..0f1e9a7 --- /dev/null +++ b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE jrcolumns ( + id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + jrtable_id INTEGER NOT NULL, + FOREIGN KEY jrtable(jrtable_id) REFERENCES jrtables(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + id_in_table INTEGER NOT NULL +); diff --git a/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql~ b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql~ new file mode 100644 index 0000000..0f1e9a7 --- /dev/null +++ b/inventur_db/migrations/2024-08-19-183025_create_jrcolumns/up.sql~ @@ -0,0 +1,9 @@ +CREATE TABLE jrcolumns ( + id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + jrtable_id INTEGER NOT NULL, + FOREIGN KEY jrtable(jrtable_id) REFERENCES jrtables(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + id_in_table INTEGER NOT NULL +); diff --git a/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql b/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql new file mode 100644 index 0000000..ecd7840 --- /dev/null +++ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql @@ -0,0 +1 @@ +DROP TABLE jrentries; diff --git a/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql~ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql b/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql new file mode 100644 index 0000000..09ac916 --- /dev/null +++ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE jrentries ( + id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, + row_pos INTEGER NOT NULL, + jrtable_id INTEGER NOT NULL, + FOREIGN KEY (jrtable_id) REFERENCES jrtables(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); diff --git a/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql~ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-21-180845_create_jrentry/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql b/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql new file mode 100644 index 0000000..3af7819 --- /dev/null +++ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql @@ -0,0 +1 @@ +DROP TABLE jrcells; diff --git a/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql~ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql b/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql new file mode 100644 index 0000000..c7091d8 --- /dev/null +++ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE jrcells ( + id INTEGER AUTO_INCREMENT NOT NULL, + PRIMARY KEY(id), + cell_value VARCHAR(2048) NOT NULL, + jrentry_id INTEGER NOT NULL, + FOREIGN KEY (jrentry_id) REFERENCES jrentries(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + jrcolumn_id INTEGER NOT NULL, + FOREIGN KEY (jrcolumn_id) REFERENCES jrcolumns(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); diff --git a/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql~ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-21-185127_create_jrcell/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql new file mode 100644 index 0000000..3df77ef --- /dev/null +++ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE jrtables + ADD num_fields INTEGER NOT NULL; diff --git a/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql~ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql new file mode 100644 index 0000000..c49cac8 --- /dev/null +++ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE jrtables + DROP COLUMN num_fields; diff --git a/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql~ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-22-171815_drop_num_fields/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/migrations/2024-08-22-203903_create_users/down.sql b/inventur_db/migrations/2024-08-22-203903_create_users/down.sql new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/inventur_db/migrations/2024-08-22-203903_create_users/down.sql @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/inventur_db/migrations/2024-08-22-203903_create_users/down.sql~ b/inventur_db/migrations/2024-08-22-203903_create_users/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-22-203903_create_users/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-22-203903_create_users/up.sql b/inventur_db/migrations/2024-08-22-203903_create_users/up.sql new file mode 100644 index 0000000..e71bc19 --- /dev/null +++ b/inventur_db/migrations/2024-08-22-203903_create_users/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + id INTEGER AUTO_INCREMENT NOT NULL, + PRIMARY KEY(id), + username VARCHAR(255) NOT NULL, + email VARCHAR(512) NOT NULL +); diff --git a/inventur_db/migrations/2024-08-22-203903_create_users/up.sql~ b/inventur_db/migrations/2024-08-22-203903_create_users/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-22-203903_create_users/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql new file mode 100644 index 0000000..8778314 --- /dev/null +++ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql @@ -0,0 +1,5 @@ +SET FOREIGN_KEY_CHECKS=0; +UPDATE TABLE jrtables + DROP COLUMN owner_id + DROP COLUMN fk_owner_id; +SET FOREIGN_KEY_CHECKS=1; diff --git a/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql~ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql new file mode 100644 index 0000000..2f72bb3 --- /dev/null +++ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql @@ -0,0 +1,10 @@ +SET FOREIGN_KEY_CHECKS=0; +ALTER TABLE jrtables + ADD owner_id INTEGER NOT NULL; +ALTER TABLE jrtables + ADD CONSTRAINT fk_owner_id + FOREIGN KEY (owner_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE; +SET FOREIGN_KEY_CHECKS=1; + diff --git a/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql~ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-22-205948_add_owner_to_table/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql b/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql new file mode 100644 index 0000000..91fa6b0 --- /dev/null +++ b/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE jrcolumns + DROP COLUMN column_type; diff --git a/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql~ b/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql~ new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/inventur_db/migrations/2024-08-27-120510_add_column_type/down.sql~ @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql b/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql new file mode 100644 index 0000000..5f50c1b --- /dev/null +++ b/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE jrcolumns + ADD column_type INTEGER NOT NULL DEFAULT 0; diff --git a/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql~ b/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql~ new file mode 100644 index 0000000..b37f10d --- /dev/null +++ b/inventur_db/migrations/2024-08-27-120510_add_column_type/up.sql~ @@ -0,0 +1 @@ +-- Your SQL goes here diff --git a/inventur_db/src/bin/add_cell.rs~ b/inventur_db/src/bin/add_cell.rs~ new file mode 100644 index 0000000..7bd8dad --- /dev/null +++ b/inventur_db/src/bin/add_cell.rs~ @@ -0,0 +1,28 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(3) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + let value = args() + .nth(4) + .expect("Usage: add_cell ."); + + println!("{:?}", create_jrcell_relative(&mut establish_connection(), tblid, rowpos, idintbl, value)); +} diff --git a/inventur_db/src/bin/add_column.rs b/inventur_db/src/bin/add_column.rs new file mode 100644 index 0000000..89ecddf --- /dev/null +++ b/inventur_db/src/bin/add_column.rs @@ -0,0 +1,21 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let table_id = args() + .nth(1) + .expect("Usage: add_column .") + .parse::() + .expect("Usage: add_column ."); + let name = args() + .nth(2) + .expect("Usage: add_column ."); + let uid = args() + .nth(3) + .expect("Usage: add_column .") + .parse::() + .expect("Usage: add_column ."); + + let conn = &mut establish_connection(); + println!("{}", add_column(conn, table_id, name, uid).expect("Error")); +} diff --git a/inventur_db/src/bin/add_column.rs~ b/inventur_db/src/bin/add_column.rs~ new file mode 100644 index 0000000..15d8166 --- /dev/null +++ b/inventur_db/src/bin/add_column.rs~ @@ -0,0 +1,15 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let name = args() + .nth(1) + .expect("Name missing"); + let table_id = args() + .nth(2) + .expect("Table_id missing.") + .parse::() + .expect("Expected a number as table id."); + let conn = &mut establish_connection(); + jrcolumns::create_jrcolumn(conn, name, table_id); +} diff --git a/inventur_db/src/bin/add_entry.rs b/inventur_db/src/bin/add_entry.rs new file mode 100644 index 0000000..3254a59 --- /dev/null +++ b/inventur_db/src/bin/add_entry.rs @@ -0,0 +1,36 @@ +use inventur_db::*; +use std::env::args; +use std::io::{stdin, Read}; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: add_entry .") + .parse::() + .expect("Usage: add_entry ."); + + let uid = args() + .nth(2) + .expect("Usage: add_entry .") + .parse::() + .expect("Usage: add_entry ."); + + let conn = &mut establish_connection(); + + let tbl = get_table(conn, tblid, uid); + if tbl.is_none() { + panic!("Table not found."); + } + let tbl = tbl.unwrap(); + let mut cells = Vec::new(); + for clm in tbl.column_names { + println!("{}:", clm); + let mut cell = String::new(); + stdin().read_line(&mut cell); + cells.push(cell.clone()); + } + + + println!("{}", add_row(conn, tblid, cells, uid).unwrap()); + +} diff --git a/inventur_db/src/bin/add_entry.rs~ b/inventur_db/src/bin/add_entry.rs~ new file mode 100644 index 0000000..e7445fe --- /dev/null +++ b/inventur_db/src/bin/add_entry.rs~ @@ -0,0 +1,36 @@ +use inventur_db::*; +use std::env::args; +use std::io::{stdin, Read}; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: add_entry .") + .parse::() + .expect("Usage: add_entry ."); + + let uid = args() + .nth(2) + .expect("Usage: add_entry .") + .parse::() + .expect("Usage: add_entry ."); + + let conn = &mut establish_connection(); + + let tbl = get_table(conn, tblid, uid); + if tbl.is_none() { + panic!("Table not found."); + } + let tbl = tbl.unwrap(); + let mut rows = Vec::new(); + for clm in tbl.column_names { + println!("{}:", clm); + let mut cell = String::new(); + stdin().read_line(&mut cell); + rows.push(cell.clone()); + } + + + println!("{}", add_row(conn, tblid, rows, uid).expect("Error")); + +} diff --git a/inventur_db/src/bin/change_cell_value.rs b/inventur_db/src/bin/change_cell_value.rs new file mode 100644 index 0000000..3e4c0ab --- /dev/null +++ b/inventur_db/src/bin/change_cell_value.rs @@ -0,0 +1,34 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(3) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let newvalue = args() + .nth(4) + .expect("Usage: change_cell_value ."); + + let uid = args() + .nth(5) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Usage: change_cell_value ."); + + println!("{:?}", edit_cell(&mut establish_connection(), tblid, rowpos, idintbl, newvalue, uid)); +} diff --git a/inventur_db/src/bin/change_cell_value.rs~ b/inventur_db/src/bin/change_cell_value.rs~ new file mode 100644 index 0000000..f125f0e --- /dev/null +++ b/inventur_db/src/bin/change_cell_value.rs~ @@ -0,0 +1,28 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(3) + .expect("Usage: change_cell_value .") + .parse::() + .expect("Expected a number."); + + let newvalue = args() + .nth(4) + .expect("Usage: change_cell_value ."); + + println!("{:?}", jrcells::change_jrcell_value_relative(&mut establish_connection(), tblid, rowpos, idintbl, newvalue)); +} diff --git a/inventur_db/src/bin/create_table.rs b/inventur_db/src/bin/create_table.rs new file mode 100644 index 0000000..13704f6 --- /dev/null +++ b/inventur_db/src/bin/create_table.rs @@ -0,0 +1,38 @@ +use inventur_db::*; +use std::io::stdin; + +fn main() { + let conn = &mut establish_connection(); + + let mut name = String::new(); + let mut num_fields = String::new(); + let mut uid = String::new(); + + println!("Name: "); + stdin().read_line(&mut name); + let name = name.trim(); + println!("Number of fields: "); + stdin().read_line(&mut num_fields); + println!("Userid:"); + stdin().read_line(&mut uid); + let uid = uid + .trim() + .parse::() + .expect("Number expected."); + let num_fields = num_fields + .trim() + .parse::() + .expect("Number expected"); + + let mut fields : Vec = Vec::new(); + + for i in 0..num_fields { + let mut field = String::new(); + stdin().read_line(&mut field); + let field = field.trim(); + fields.push(field.to_string()); + } + + + println!("{}", create_table(conn, name.to_string(), fields, uid).unwrap()); +} diff --git a/inventur_db/src/bin/create_table.rs~ b/inventur_db/src/bin/create_table.rs~ new file mode 100644 index 0000000..c5f8678 --- /dev/null +++ b/inventur_db/src/bin/create_table.rs~ @@ -0,0 +1,38 @@ +use inventur_db::*; +use std::io::stdin; + +fn main() { + let conn = &mut establish_connection(); + + let mut name = String::new(); + let mut num_fields = String::new(); + let mut uid = String::new(); + + println!("Name: "); + stdin().read_line(&mut name).unwrap(); + let name = name.trim_end(); + println!("Number of fields: "); + stdin().read_line(&mut num_fields); + println!("Userid:"); + stdin().read_line(&mut uid); + let uid = uid + .trim() + .parse::() + .expect("Number expected."); + let num_fields = num_fields + .trim() + .parse::() + .expect("Number expected"); + + let mut fields = Vec::new(); + + for i in 0..num_fields { + let mut field = String::new(); + stdin().read_line(&mut field); + field.trim(); + fields.push(field); + } + + + println!("{}", create_table(conn, name.to_string(), fields, uid).unwrap()); +} diff --git a/inventur_db/src/bin/create_user.rs b/inventur_db/src/bin/create_user.rs new file mode 100644 index 0000000..2150c8b --- /dev/null +++ b/inventur_db/src/bin/create_user.rs @@ -0,0 +1,14 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let uname = args() + .nth(1) + .expect("Usage: create_user ."); + + let mail = args() + .nth(2) + .expect("Usage: create_user ."); + + println!("{}", register_or_login(&mut establish_connection(), uname, mail).unwrap().uid); +} diff --git a/inventur_db/src/bin/create_user.rs~ b/inventur_db/src/bin/create_user.rs~ new file mode 100644 index 0000000..2150c8b --- /dev/null +++ b/inventur_db/src/bin/create_user.rs~ @@ -0,0 +1,14 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let uname = args() + .nth(1) + .expect("Usage: create_user ."); + + let mail = args() + .nth(2) + .expect("Usage: create_user ."); + + println!("{}", register_or_login(&mut establish_connection(), uname, mail).unwrap().uid); +} diff --git a/inventur_db/src/bin/delete_cell.rs~ b/inventur_db/src/bin/delete_cell.rs~ new file mode 100644 index 0000000..a988326 --- /dev/null +++ b/inventur_db/src/bin/delete_cell.rs~ @@ -0,0 +1,24 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(3) + .expect("Usage: add_cell .") + .parse::() + .expect("Expected a number."); + + println!("{:?}", delete_jrcell_relative(&mut establish_connection(), tblid, rowpos, idintbl)); +} diff --git a/inventur_db/src/bin/delete_column.rs b/inventur_db/src/bin/delete_column.rs new file mode 100644 index 0000000..6a57441 --- /dev/null +++ b/inventur_db/src/bin/delete_column.rs @@ -0,0 +1,26 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: delete_column .") + .parse::() + .expect("Usage: delete_column ."); + + let idintbl = args() + .nth(2) + .expect("Usage: delete_column .") + .parse::() + .expect("Usage: delete_column ."); + + let uid = args() + .nth(3) + .expect("Usage: delete_column .") + .parse::() + .expect("Usage: delete_column ."); + + let conn = &mut establish_connection(); + + println!("{:?}", delete_column(conn, tblid, idintbl, uid)); +} diff --git a/inventur_db/src/bin/delete_column.rs~ b/inventur_db/src/bin/delete_column.rs~ new file mode 100644 index 0000000..a22909f --- /dev/null +++ b/inventur_db/src/bin/delete_column.rs~ @@ -0,0 +1,20 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Table id missing.") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(2) + .expect("Id in table missing.") + .parse::() + .expect("Expected a number."); + + let conn = &mut establish_connection(); + + println!("{:?}", jrcolumns::delete_column_relative(conn, tblid, idintbl)); +} diff --git a/inventur_db/src/bin/delete_entry.rs b/inventur_db/src/bin/delete_entry.rs new file mode 100644 index 0000000..eb5b0d1 --- /dev/null +++ b/inventur_db/src/bin/delete_entry.rs @@ -0,0 +1,25 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: delete_entry ") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: delete_entry ") + .parse::() + .expect("Expected a number."); + + let uid = args() + .nth(3) + .expect("Usage: delete_entry ") + .parse::() + .expect("Usage: delete_entry "); + + delete_row(&mut establish_connection(), tblid, rowpos, uid); + +} diff --git a/inventur_db/src/bin/delete_entry.rs~ b/inventur_db/src/bin/delete_entry.rs~ new file mode 100644 index 0000000..4ccf63f --- /dev/null +++ b/inventur_db/src/bin/delete_entry.rs~ @@ -0,0 +1,19 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: delete_entry ") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: delete_entry ") + .parse::() + .expect("Expected a number."); + + jrentries::delete_jrentry_relative(&mut establish_connection(), tblid, rowpos); + +} diff --git a/inventur_db/src/bin/delete_table.rs b/inventur_db/src/bin/delete_table.rs new file mode 100644 index 0000000..29cf8e0 --- /dev/null +++ b/inventur_db/src/bin/delete_table.rs @@ -0,0 +1,17 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: delete_jrtable .") + .parse::() + .expect("Expected a number."); + let uid = args() + .nth(2) + .expect("Usage: delete_jrtable .") + .parse::() + .expect("Usage: delete_jrtable ."); + + println!("{:?}", delete_table(&mut establish_connection(), tblid, uid)); +} diff --git a/inventur_db/src/bin/delete_table.rs~ b/inventur_db/src/bin/delete_table.rs~ new file mode 100644 index 0000000..33b928c --- /dev/null +++ b/inventur_db/src/bin/delete_table.rs~ @@ -0,0 +1,12 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: delete_jrtable .") + .parse::() + .expect("Expected a number."); + + println!("{:?}", jrtables::delete_jrtable(&mut establish_connection(), tblid)); +} diff --git a/inventur_db/src/bin/delete_user.rs b/inventur_db/src/bin/delete_user.rs new file mode 100644 index 0000000..45c064e --- /dev/null +++ b/inventur_db/src/bin/delete_user.rs @@ -0,0 +1,13 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let uid = args() + .nth(1) + .expect("Missing argument.") + .parse::() + .expect(""); + + let conn = &mut establish_connection(); + delete_user(conn, uid); +} diff --git a/inventur_db/src/bin/delete_user.rs~ b/inventur_db/src/bin/delete_user.rs~ new file mode 100644 index 0000000..197dd05 --- /dev/null +++ b/inventur_db/src/bin/delete_user.rs~ @@ -0,0 +1,19 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let arg = args() + .nth(1) + .expect("Missing argument."); + + let conn = &mut establish_connection(); + let mut uid = users::get_uid_uname(conn, &arg); + if uid.is_err() { + uid = users::get_uid_email(conn, &arg); + } + if uid.is_err() { + panic!("User not found!"); + } + let uid = uid.unwrap(); + delete_user(conn, uid); +} diff --git a/inventur_db/src/bin/move_column.rs b/inventur_db/src/bin/move_column.rs new file mode 100644 index 0000000..8265f0b --- /dev/null +++ b/inventur_db/src/bin/move_column.rs @@ -0,0 +1,31 @@ +use std::env::args; +use inventur_db::*; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: move_column .") + .parse::() + .expect("Usage: move_column ."); + + let idintbl = args() + .nth(2) + .expect("Usage: move_column .") + .parse::() + .expect("Usage: move_column ."); + + let newidintbl = args() + .nth(3) + .expect("Usage: move_column .") + .parse::() + .expect("Usage: move_column ."); + + let uid = args() + .nth(4) + .expect("Usage: move_column .") + .parse::() + .expect("Usage: move_column ."); + + let conn = &mut establish_connection(); + move_column(conn, tblid, idintbl, newidintbl, uid); +} diff --git a/inventur_db/src/bin/move_column.rs~ b/inventur_db/src/bin/move_column.rs~ new file mode 100644 index 0000000..f89a7ac --- /dev/null +++ b/inventur_db/src/bin/move_column.rs~ @@ -0,0 +1,25 @@ +use std::env::args; +use inventur_db::*; + +fn main() { + let tblid = args() + .nth(1) + .expect("Table id missing.") + .parse::() + .expect("Expected a number."); + + let idintbl = args() + .nth(2) + .expect("Id in table missing.") + .parse::() + .expect("Expected a number."); + + let newidintbl = args() + .nth(3) + .expect("New id in table missing.") + .parse::() + .expect("Expected a number."); + + let conn = &mut establish_connection(); + jrcolumns::move_column_relative(conn, tblid, idintbl, newidintbl); +} diff --git a/inventur_db/src/bin/move_entry.rs b/inventur_db/src/bin/move_entry.rs new file mode 100644 index 0000000..05097d4 --- /dev/null +++ b/inventur_db/src/bin/move_entry.rs @@ -0,0 +1,31 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + let newpos = args() + .nth(3) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + let uid = args() + .nth(4) + .expect("Usage: move_entry .") + .parse::() + .expect("Usage: move_entry ."); + + println!("{:?}", move_row(&mut establish_connection(), tblid, rowpos, newpos, uid)); + +} diff --git a/inventur_db/src/bin/move_entry.rs~ b/inventur_db/src/bin/move_entry.rs~ new file mode 100644 index 0000000..6d3ae3b --- /dev/null +++ b/inventur_db/src/bin/move_entry.rs~ @@ -0,0 +1,25 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + let rowpos = args() + .nth(2) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + let newpos = args() + .nth(3) + .expect("Usage: move_entry .") + .parse::() + .expect("Expected a number."); + + println!("{:?}", jrentries::move_jrentry(&mut establish_connection(), tblid, rowpos, newpos)); + +} diff --git a/inventur_db/src/bin/rename_column.rs b/inventur_db/src/bin/rename_column.rs new file mode 100644 index 0000000..f94ac9a --- /dev/null +++ b/inventur_db/src/bin/rename_column.rs @@ -0,0 +1,30 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let table_id = args() + .nth(1) + .expect("Usage: rename_column .") + .parse::() + .expect("Usage: rename_column ."); + + let id_in_table = args() + .nth(2) + .expect("Usage: rename_column .") + .parse::() + .expect("Usage: rename_column ."); + + let name = args() + .nth(3) + .expect("Usage: rename_column ."); + + let uid = args() + .nth(4) + .expect("Usage: rename_column .") + .parse::() + .expect("Usage: rename_column ."); + + let conn = &mut establish_connection(); + + rename_column(conn, table_id, id_in_table, name, uid); +} diff --git a/inventur_db/src/bin/rename_column.rs~ b/inventur_db/src/bin/rename_column.rs~ new file mode 100644 index 0000000..cd5b841 --- /dev/null +++ b/inventur_db/src/bin/rename_column.rs~ @@ -0,0 +1,27 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + if args().len() != 4 { + panic!("Usage: rename_column ."); + } + let table_id = args() + .nth(1) + .unwrap() + .parse::() + .expect("Table id must be a number."); + + let id_in_table = args() + .nth(2) + .unwrap() + .parse::() + .expect("id in table must be a number."); + + let name = args() + .nth(3) + .unwrap(); + + let conn = &mut establish_connection(); + + jrcolumns::rename_column_relative(conn, table_id, id_in_table, name); +} diff --git a/inventur_db/src/bin/rename_jrtable.rs b/inventur_db/src/bin/rename_jrtable.rs new file mode 100644 index 0000000..06b951c --- /dev/null +++ b/inventur_db/src/bin/rename_jrtable.rs @@ -0,0 +1,23 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let table_id = args() + .nth(1) + .expect("Usage: rename_jrtable .") + .parse::() + .expect("number expected for id."); + + let new_name = args() + .nth(2) + .expect("Usage: rename_jrtable ."); + + let uid = args() + .nth(3) + .expect("Usage: rename_jrtable .") + .parse::() + .expect("Usage: rename_jrtable ."); + + let conn = &mut establish_connection(); + rename_table(conn, table_id, new_name, uid); +} diff --git a/inventur_db/src/bin/rename_jrtable.rs~ b/inventur_db/src/bin/rename_jrtable.rs~ new file mode 100644 index 0000000..1919592 --- /dev/null +++ b/inventur_db/src/bin/rename_jrtable.rs~ @@ -0,0 +1,23 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let table_id = args() + .nth(1) + .expect("Usage: rename_jrtable .") + .parse::() + .expect("number expected for id."); + + let uid = args() + .nth(2) + .expect("Usage: rename_jrtable .") + .parse::() + .expect("Usage: rename_jrtable ."); + + let new_name = args() + .nth(3) + .expect("Usage: rename_jrtable ."); + + let conn = &mut establish_connection(); + rename_table(conn, table_id, uid, new_name); +} diff --git a/inventur_db/src/bin/show_table.rs b/inventur_db/src/bin/show_table.rs new file mode 100644 index 0000000..a408770 --- /dev/null +++ b/inventur_db/src/bin/show_table.rs @@ -0,0 +1,36 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: show_table .") + .parse::() + .expect("Usage: show_table ."); + + let uid = args() + .nth(2) + .expect("Usage: show_table .") + .parse::() + .expect("Usage: show_table ."); + + let tbl = get_table(&mut establish_connection(), tblid, uid); + if tbl.is_none() { + panic!("Couldn't get table."); + } + let tbl = tbl.unwrap(); + println!("Table {}: {}", tbl.tblid, tbl.name.trim()); + println!(); + print!("|"); + for clm in tbl.column_names { + print!(" {} |", clm.trim()); + } + println!(); + for row in tbl.rows { + print!("|"); + for cell in row.cells { + print!(" {} |", cell.trim()); + } + println!(); + } +} diff --git a/inventur_db/src/bin/show_table.rs~ b/inventur_db/src/bin/show_table.rs~ new file mode 100644 index 0000000..cbdf684 --- /dev/null +++ b/inventur_db/src/bin/show_table.rs~ @@ -0,0 +1,29 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let tblid = args() + .nth(1) + .expect("Usage: show_table .") + .parse::() + .expect("Expected a number."); + let tbl = get_table(&mut establish_connection(), tblid, 1); + if tbl.is_none() { + panic!("Couldn't get table."); + } + let tbl = tbl.unwrap(); + println!("Table {}: {}", tbl.tblid, tbl.name); + println!(); + print!("|"); + for clm in tbl.column_names { + print!(" {} |", clm); + } + println!(); + for row in tbl.rows { + print!("|"); + for cell in row.cells { + print!(" {} |", cell); + } + println!(); + } +} diff --git a/inventur_db/src/bin/show_tables.rs~ b/inventur_db/src/bin/show_tables.rs~ new file mode 100644 index 0000000..55112bd --- /dev/null +++ b/inventur_db/src/bin/show_tables.rs~ @@ -0,0 +1,16 @@ +use inventur_db::*; + +fn main() { + + let conn = &mut establish_connection(); + let results = get_all_tables(conn).expect("Error loading tables."); + + println!("Total: {} tables.", results.len()); + for table in results { + println!("{}: {} has {} columns.", table.id, table.name, table.num_fields); + let columns = get_columns_of(conn, table.id).expect(format!("Error reading columns for table {}.", table.id).as_str()); + for column in columns { + print!("{} ", column.name); + } + } +} diff --git a/inventur_db/src/bin/show_user_tblids.rs b/inventur_db/src/bin/show_user_tblids.rs new file mode 100644 index 0000000..e6758df --- /dev/null +++ b/inventur_db/src/bin/show_user_tblids.rs @@ -0,0 +1,11 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let uid = args() + .nth(1) + .expect("Usage: show_user_tblids .") + .parse::() + .expect("Usage: show_user_tblids ."); + println!("{:?}", get_user_tblids(&mut establish_connection(), uid).unwrap()); +} diff --git a/inventur_db/src/bin/show_user_tblids.rs~ b/inventur_db/src/bin/show_user_tblids.rs~ new file mode 100644 index 0000000..737ef9c --- /dev/null +++ b/inventur_db/src/bin/show_user_tblids.rs~ @@ -0,0 +1,10 @@ +use inventur_db::*; +use std::env::args; + +fn main() { + let uid = args() + .nth(1) + .parse::() + .expect("Usage: show_user_tblids ."); + println!("{:?}", get_user_tblids(uid)); +} diff --git a/inventur_db/src/jrcells.rs b/inventur_db/src/jrcells.rs new file mode 100644 index 0000000..82dcfa5 --- /dev/null +++ b/inventur_db/src/jrcells.rs @@ -0,0 +1,98 @@ +use crate::schema; +use crate::models; +use crate::jrcolumns; +use crate::jrentries; + +use models::{ Jrcell, NewJrcell }; +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 { + 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 { + 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(); + change_jrcell_value(conn, ntrid, clmid, new_value) +} + +pub fn delete_jrcell_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, idintbl: i32) -> Result { + 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 { + use self::schema::jrentries::dsl::jrentries; + use self::schema::jrcolumns::dsl::jrcolumns; + + let ntr = jrentries + .find(entryid) + .execute(conn); + if ntr.is_err() { + return ntr; + } + + let clmn = jrcolumns + .find(columnid) + .execute(conn); + if clmn.is_err() { + return clmn; + } + + let cell = NewJrcell { cell_value: value.to_string(), jrentry_id: entryid, jrcolumn_id: columnid }; + diesel::insert_into(crate::schema::jrcells::table) + .values(cell) + .execute(conn) + + +} + +pub fn change_jrcell_value(conn: &mut MysqlConnection, entryid: i32, columnid: i32, new_value: String) -> Result { + diesel::update(jrcells.filter(jrentry_id.eq(entryid)).filter(jrcolumn_id.eq(columnid))) + .set(cell_value.eq(new_value)) + .execute(conn) +} + +pub fn delete_jrcell(conn: &mut MysqlConnection, entryid: i32, columnid: i32) -> Result { + 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, diesel::result::Error> { + jrcells + .filter(jrentry_id.eq(entryid)) + .select(Jrcell::as_select()) + .load(conn) +} diff --git a/inventur_db/src/jrcells.rs~ b/inventur_db/src/jrcells.rs~ new file mode 100644 index 0000000..8c9d020 --- /dev/null +++ b/inventur_db/src/jrcells.rs~ @@ -0,0 +1,98 @@ +use crate::schema; +use crate::models; +use crate::jrcolumns; +use crate::jrentries; + +use models::{ Jrcell, NewJrcell }; +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 { + 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 { + 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(); + change_jrcell_value(conn, ntrid, clmid, new_value) +} + +pub fn delete_jrcell_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, idintbl: i32) -> Result { + 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 { + use self::schema::jrentries::dsl::jrentries; + use self::schema::jrcolumns::dsl::jrcolumns; + + let ntr = jrentries + .find(entryid) + .execute(conn); + if ntr.is_err() { + return ntr; + } + + let clmn = jrcolumns + .find(columnid) + .execute(conn); + if clmn.is_err() { + return clmn; + } + + let cell = NewJrcell { cell_value: value, jrentry_id: entryid, jrcolumn_id: columnid }; + diesel::insert_into(crate::schema::jrcells::table) + .values(cell) + .execute(conn) + + +} + +pub fn change_jrcell_value(conn: &mut MysqlConnection, entryid: i32, columnid: i32, new_value: String) -> Result { + diesel::update(jrcells.filter(jrentry_id.eq(entryid)).filter(jrcolumn_id.eq(columnid))) + .set(cell_value.eq(new_value)) + .execute(conn) +} + +pub fn delete_jrcell(conn: &mut MysqlConnection, entryid: i32, columnid: i32) -> Result { + 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, diesel::result::Error> { + jrcells + .filter(jrentry_id.eq(entryid)) + .select(Jrcell::as_select()) + .load(conn) +} diff --git a/inventur_db/src/jrcolumns.rs b/inventur_db/src/jrcolumns.rs new file mode 100644 index 0000000..7f23ea3 --- /dev/null +++ b/inventur_db/src/jrcolumns.rs @@ -0,0 +1,217 @@ +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; +use crate::models; +use crate::schema; +use crate::jrtables; +use models::{ Jrcolumn, NewJrcolumn }; +use schema::jrcolumns::dsl::{ jrcolumns, id, name, jrtable_id, id_in_table }; + + +pub fn create_jrcolumn(conn: &mut MysqlConnection, tblid: i32, clmnname: String) -> Result { + use schema::jrentries::dsl::{ jrentries, jrtable_id }; + use models::{ Jrentry, NewJrcell }; + + let ncols = jrtables::get_ncols(conn, tblid); + if ncols.is_err() { + return ncols; + } + let ncols = ncols.unwrap() as i32; + let jrcolumn = NewJrcolumn { name: clmnname, jrtable_id: tblid, id_in_table: ncols + 1 }; + let res = diesel::insert_into(crate::schema::jrcolumns::table) + .values(&jrcolumn) + .execute(conn); + if res.is_err() { + return res; + } + + let colid = get_clmid_relative(conn, tblid, ncols+1); + if colid.is_err() { + return Err(colid.err().unwrap()); + } + let colid = colid.unwrap(); + let rows = jrentries + .filter(jrtable_id.eq(tblid)) + .select(Jrentry::as_select()) + .load(conn); + if rows.is_err() { + return Err(rows.err().unwrap()); + } + let rows = rows.unwrap(); + let mut cells = Vec::new(); + for row in rows { + cells.push(NewJrcell { jrentry_id: row.id, jrcolumn_id: colid, cell_value: "".to_string() }); + } + diesel::insert_into(crate::schema::jrcells::table) + .values(&cells) + .execute(conn) + + +} + +pub fn create_jrcolumns_empty_tbl(conn: &mut MysqlConnection, tblid: i32, names: Vec) -> Result { + let mut cols : Vec = Vec::new(); + let ncols = jrtables::get_ncols(conn, tblid); + if ncols.is_err() { + return ncols; + } + let mut ncols = ncols.unwrap() as i32; + for clmnname in names { + ncols += 1; + cols.push(NewJrcolumn { name: clmnname, jrtable_id: tblid, id_in_table: ncols}); + } + let cols = cols; + + diesel::insert_into(crate::schema::jrcolumns::table) + .values(&cols) + .execute(conn) + +} + +pub fn get_last_column_id_of(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .order(id_in_table.desc()) + .select(id) + .first::(conn) +} + +pub fn get_clmid_relative(conn: &mut MysqlConnection, tblid: i32, idintbl: i32) -> Result { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .filter(id_in_table.eq(idintbl)) + .select(id) + .first::(conn) +} + +pub fn rename_column_relative(conn: &mut MysqlConnection, tblid: i32, id_in_tbl: i32, new_name: String) -> Result { + let clmid = get_clmid_relative(conn, tblid, id_in_tbl); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + rename_column(conn, clmid.unwrap(), new_name) +} + +pub fn rename_column(conn: &mut MysqlConnection, clmnid: i32, new_name: String) -> Result { + diesel::update(jrcolumns.find(clmnid)) + .set(name.eq(new_name)) + .execute(conn) +} + +pub fn move_column_relative(conn: &mut MysqlConnection, tblid: i32, id_in_tbl: i32, new_id_in_tbl: i32) -> Result { + let clmid = get_clmid_relative(conn, tblid, id_in_tbl); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + move_column(conn, clmid.unwrap(), new_id_in_tbl) +} + +pub fn move_column(conn: &mut MysqlConnection, clmnid: i32, new_id_in_table: i32) -> Result{ + let tbl_id = jrcolumns + .find(clmnid) + .select(jrtable_id) + .first::(conn); + + if tbl_id.is_err() { + return Err(tbl_id.err().unwrap()); + } + let tbl_id = tbl_id.unwrap(); + + let cols = jrcolumns + .filter(jrtable_id.eq(tbl_id)) + .order(id_in_table.asc()) + .select(Jrcolumn::as_select()) + .load(conn); + + if cols.is_err() { + return Err(cols.err().unwrap()); + } + let cols = cols.unwrap(); + let ncols = cols.len() as i32; + + if new_id_in_table > ncols || new_id_in_table < 0 { + return Err(diesel::result::Error::NotFound); + } + + let cur_id_in_table = + jrcolumns + .find(clmnid) + .select(id_in_table) + .first::(conn); + + if cur_id_in_table.is_err() { + return Err(cur_id_in_table.err().unwrap()); + } + let cur_id_in_table = cur_id_in_table.unwrap(); + + let dir; + let a; + let b; + if cur_id_in_table < new_id_in_table { + dir = -1; + a = cur_id_in_table + 1; + b = new_id_in_table; + } else { + dir = 1; + a = new_id_in_table; + b = cur_id_in_table - 1; + } + + for j in a..=b { + let upd = diesel::update(jrcolumns.find(cols[(j-1) as usize].id)) + .set(id_in_table.eq(id_in_table + dir)) + .execute(conn); + if upd.is_err() { + return upd; + } + } + diesel::update(jrcolumns.find(clmnid)) + .set(id_in_table.eq(new_id_in_table)) + .execute(conn) +} + +pub fn delete_column_relative(conn: &mut MysqlConnection, tblid: i32, idinclm: i32) -> Result { + let clmid = get_clmid_relative(conn, tblid, idinclm); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + delete_column(conn, clmid.unwrap()) +} + +pub fn delete_column(conn: &mut MysqlConnection, clmnid: i32) -> Result { + let tblid = jrcolumns + .find(clmnid) + .select(jrtable_id) + .first::(conn); + + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + let tblid = tblid.unwrap(); + + let ncols = jrcolumns + .filter(jrtable_id.eq(tblid)) + .execute(conn); + + if ncols.is_err() { + return ncols; + } + let ncols = ncols.unwrap() as i32; + + let mv = move_column(conn, clmnid, ncols); + + if mv.is_err() { + return mv; + } + + diesel::delete(jrcolumns.find(clmnid)) + .execute(conn) +} + +pub fn get_clmns_of(conn: &mut MysqlConnection, tblid: i32) -> Result, diesel::result::Error> { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .order(id_in_table.asc()) + .select(Jrcolumn::as_select()) + .load(conn) +} + diff --git a/inventur_db/src/jrcolumns.rs~ b/inventur_db/src/jrcolumns.rs~ new file mode 100644 index 0000000..4d4b6c9 --- /dev/null +++ b/inventur_db/src/jrcolumns.rs~ @@ -0,0 +1,197 @@ +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; +use crate::models; +use crate::schema; +use crate::jrtables; +use models::{ Jrcolumn, NewJrcolumn }; +use schema::jrcolumns::dsl::{ jrcolumns, id, name, jrtable_id, id_in_table }; + + +pub fn create_jrcolumn(conn: &mut MysqlConnection, tblid: i32, clmnname: String) -> Result { + let ncols = jrtables::get_ncols(conn, tblid); + if ncols.is_err() { + return ncols; + } + let ncols = ncols.unwrap() as i32; + let jrcolumn = NewJrcolumn { name: clmnname, jrtable_id: tblid, id_in_table: ncols + 1 }; + diesel::insert_into(crate::schema::jrcolumns::table) + .values(&jrcolumn) + .execute(conn) + +} + +pub fn create_jrcolumns(conn: &mut MysqlConnection, tblid: i32, names: Vec) -> Result { + let mut cols : Vec = Vec::new(); + let ncols = jrtables::get_ncols(conn, tblid); + if ncols.is_err() { + return ncols; + } + let mut ncols = ncols.unwrap() as i32; + for clmnname in names { + ncols += 1; + cols.push(NewJrcolumn { name: clmnname, jrtable_id: tblid, id_in_table: ncols}); + } + let cols = cols; + + diesel::insert_into(crate::schema::jrcolumns::table) + .values(&cols) + .execute(conn) + +} + +pub fn get_last_column_id_of(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .order(id_in_table.desc()) + .select(id) + .first::(conn) +} + +pub fn get_columns_of(conn: &mut MysqlConnection, tblid: i32) -> Result, diesel::result::Error> { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .order(id_in_table.asc()) + .select(Jrcolumn::as_select()) + .load(conn) +} + +pub fn get_clmid_relative(conn: &mut MysqlConnection, tblid: i32, idintbl: i32) -> Result { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .filter(id_in_table.eq(idintbl)) + .select(id) + .first::(conn) +} + +pub fn rename_column_relative(conn: &mut MysqlConnection, tblid: i32, id_in_tbl: i32, new_name: String) -> Result { + let clmid = get_clmid_relative(conn, tblid, id_in_tbl); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + rename_column(conn, clmid.unwrap(), new_name) +} + +pub fn rename_column(conn: &mut MysqlConnection, clmnid: i32, new_name: String) -> Result { + diesel::update(jrcolumns.find(clmnid)) + .set(name.eq(new_name)) + .execute(conn) +} + +pub fn move_column_relative(conn: &mut MysqlConnection, tblid: i32, id_in_tbl: i32, new_id_in_tbl: i32) -> Result { + let clmid = get_clmid_relative(conn, tblid, id_in_tbl); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + move_column(conn, clmid.unwrap(), new_id_in_tbl) +} + +pub fn move_column(conn: &mut MysqlConnection, clmnid: i32, new_id_in_table: i32) -> Result{ + let tbl_id = jrcolumns + .find(clmnid) + .select(jrtable_id) + .first::(conn); + + if tbl_id.is_err() { + return Err(tbl_id.err().unwrap()); + } + let tbl_id = tbl_id.unwrap(); + + let cols = jrcolumns + .filter(jrtable_id.eq(tbl_id)) + .order(id_in_table.asc()) + .select(Jrcolumn::as_select()) + .load(conn); + + if cols.is_err() { + return Err(cols.err().unwrap()); + } + let cols = cols.unwrap(); + let ncols = cols.len() as i32; + + if new_id_in_table > ncols || new_id_in_table < 0 { + return Err(diesel::result::Error::NotFound); + } + + let cur_id_in_table = + jrcolumns + .find(clmnid) + .select(id_in_table) + .first::(conn); + + if cur_id_in_table.is_err() { + return Err(cur_id_in_table.err().unwrap()); + } + let cur_id_in_table = cur_id_in_table.unwrap(); + + let dir; + let a; + let b; + if cur_id_in_table < new_id_in_table { + dir = -1; + a = cur_id_in_table + 1; + b = new_id_in_table; + } else { + dir = 1; + a = new_id_in_table; + b = cur_id_in_table - 1; + } + + for j in a..=b { + let upd = diesel::update(jrcolumns.find(cols[(j-1) as usize].id)) + .set(id_in_table.eq(id_in_table + dir)) + .execute(conn); + if upd.is_err() { + return upd; + } + } + diesel::update(jrcolumns.find(clmnid)) + .set(id_in_table.eq(new_id_in_table)) + .execute(conn) +} + +pub fn delete_column_relative(conn: &mut MysqlConnection, tblid: i32, idinclm: i32) -> Result { + let clmid = get_clmid_relative(conn, tblid, idinclm); + if clmid.is_err() { + return Err(clmid.err().unwrap()); + } + delete_column(conn, clmid.unwrap()) +} + +pub fn delete_column(conn: &mut MysqlConnection, clmnid: i32) -> Result { + let tblid = jrcolumns + .find(clmnid) + .select(jrtable_id) + .first::(conn); + + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + let tblid = tblid.unwrap(); + + let ncols = jrcolumns + .filter(jrtable_id.eq(tblid)) + .execute(conn); + + if ncols.is_err() { + return ncols; + } + let ncols = ncols.unwrap() as i32; + + let mv = move_column(conn, clmnid, ncols); + + if mv.is_err() { + return mv; + } + + diesel::delete(jrcolumns.find(clmnid)) + .execute(conn) +} + +pub fn get_clmns_of(conn: &mut MysqlConnection, tblid: i32) -> Result, diesel::result::Error> { + jrcolumns + .filter(jrtable_id.eq(tblid)) + .order(id_in_table.asc()) + .select(Jrcolumn::as_select()) + .load(conn) +} + diff --git a/inventur_db/src/jrentries.rs b/inventur_db/src/jrentries.rs new file mode 100644 index 0000000..0b0fd83 --- /dev/null +++ b/inventur_db/src/jrentries.rs @@ -0,0 +1,132 @@ +use crate::models; +use crate::schema; + +use schema::jrentries::dsl::{jrentries, id, jrtable_id, row_pos}; +use models::{ Jrentry, NewJrentry }; + +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; + + +pub fn create_jrentry(conn: &mut MysqlConnection, tblid: i32) -> Result { + let rowpos = jrentries + .filter(jrtable_id.eq(tblid)) + .select(row_pos) + .load::(conn); + + if rowpos.is_err() { + return Err(rowpos.err().unwrap()); + } + let rowpos = rowpos.unwrap().len() + 1; + + let ntr = NewJrentry { jrtable_id: tblid, row_pos: rowpos as i32 }; + diesel::insert_into(crate::schema::jrentries::table) + .values(&ntr) + .execute(conn) +} + +pub fn move_jrentry(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newrowpos: i32) -> Result { + let ntrids = jrentries + .filter(jrtable_id.eq(tblid)) + .order(row_pos.asc()) + .select(id) + .load::(conn); + + if ntrids.is_err() { + return Err(ntrids.err().unwrap()); + } + let ntrids = ntrids.unwrap(); + + let a; + let b; + let dir; + let entryid = ntrids[(rowpos-1) as usize]; + if rowpos < newrowpos { + a = rowpos + 1; + b = newrowpos; + dir = -1; + }else { + a = newrowpos; + b = rowpos - 1; + dir = 1; + } + + for i in a..=b { + let upd = diesel::update(jrentries.find(ntrids[(i-1) as usize])) + .set(row_pos.eq(row_pos + dir)) + .execute(conn); + if upd.is_err() { + return upd; + } + } + + diesel::update(jrentries.find(entryid)) + .set(row_pos.eq(newrowpos)) + .execute(conn) + +} + +pub fn delete_jrentry_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32) -> Result { + let entryid = jrentries + .filter(jrtable_id.eq(tblid)) + .filter(row_pos.eq(rowpos)) + .select(id) + .first::(conn); + if entryid.is_err() { + return Err(entryid.err().unwrap()); + } + delete_jrentry(conn, entryid.unwrap()) + +} + +pub fn delete_jrentry(conn: &mut MysqlConnection, entryid: i32) -> Result { + let rp = jrentries + .find(entryid) + .select(row_pos) + .first::(conn); + + let tblid = jrentries + .find(entryid) + .select(jrtable_id) + .first::(conn); + + if rp.is_err() { + return Err(tblid.err().unwrap()); + } + + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + + let rp = rp.unwrap(); + let tblid = tblid.unwrap(); + + let nr = jrentries + .filter(jrtable_id.eq(tblid)) + .execute(conn); + + if nr.is_err() { + return nr; + } + move_jrentry(conn, tblid, rp, nr.unwrap() as i32); + diesel::delete(jrentries.find(entryid)).execute(conn) + +} + +pub fn get_jrentry_id(conn: &mut MysqlConnection, tblid: i32, rowpos: i32) -> Result { + + jrentries + .filter(jrtable_id.eq(tblid)) + .filter(row_pos.eq(rowpos)) + .select(id) + .first::(conn) + +} + +pub fn get_entries_of(conn: &mut MysqlConnection, tblid: i32) -> Result, diesel::result::Error> { + jrentries + .filter(jrtable_id.eq(tblid)) + .order(row_pos.asc()) + .select(Jrentry::as_select()) + .load(conn) +} diff --git a/inventur_db/src/jrentries.rs~ b/inventur_db/src/jrentries.rs~ new file mode 100644 index 0000000..7404504 --- /dev/null +++ b/inventur_db/src/jrentries.rs~ @@ -0,0 +1,131 @@ +use crate::models; +use crate::schema; + +use schema::jrentries::dsl::{jrentries, id, jrtable_id, row_pos}; +use models::{ Jrentry, NewJrentry }; + +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; + + +pub fn create_jrentry(conn: &mut MysqlConnection, tblid: i32) -> Result { + let rowpos = jrentries + .filter(jrtable_id.eq(tblid)) + .select(row_pos) + .load::(conn); + + if rowpos.is_err() { + return Err(rowpos.err().unwrap()); + } + let rowpos = rowpos.unwrap().len() + 1; + + let ntr = NewJrentry { jrtable_id: tblid, row_pos: rowpos as i32 }; + diesel::insert_into(crate::schema::jrentries::table) + .values(ntr) + .execute(conn) +} + +pub fn move_jrentry(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newrowpos: i32) -> Result { + let ntrids = jrentries + .filter(jrtable_id.eq(tblid)) + .order(row_pos.asc()) + .select(id) + .load::(conn); + + if ntrids.is_err() { + return Err(ntrids.err().unwrap()); + } + let ntrids = ntrids.unwrap(); + + let a; + let b; + let dir; + let entryid = ntrids[(rowpos-1) as usize]; + if rowpos < newrowpos { + a = rowpos + 1; + b = newrowpos; + dir = -1; + }else { + a = newrowpos; + b = rowpos - 1; + dir = 1; + } + + for i in a..=b { + let upd = diesel::update(jrentries.find(ntrids[(i-1) as usize])) + .set(row_pos.eq(row_pos + dir)) + .execute(conn); + if upd.is_err() { + return upd; + } + } + + diesel::update(jrentries.find(entryid)) + .set(row_pos.eq(newrowpos)) + .execute(conn) + +} + +pub fn delete_jrentry_relative(conn: &mut MysqlConnection, tblid: i32, rowpos: i32) -> Result { + let entryid = jrentries + .filter(jrtable_id.eq(tblid)) + .filter(row_pos.eq(rowpos)) + .select(id) + .first::(conn); + if entryid.is_err() { + return Err(entryid.err().unwrap()); + } + delete_jrentry(conn, entryid.unwrap()) + +} + +pub fn delete_jrentry(conn: &mut MysqlConnection, entryid: i32) -> Result { + let rp = jrentries + .find(entryid) + .select(row_pos) + .first::(conn); + + let tblid = jrentries + .find(entryid) + .select(jrtable_id) + .first::(conn); + + if rp.is_err() { + return Err(tblid.err().unwrap()); + } + + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + + let rp = rp.unwrap(); + let tblid = tblid.unwrap(); + + let nr = jrentries + .filter(jrtable_id.eq(tblid)) + .execute(conn); + + if nr.is_err() { + return nr; + } + move_jrentry(conn, tblid, rp, nr.unwrap() as i32); + diesel::delete(jrentries.find(entryid)).execute(conn) + +} + +pub fn get_jrentry_id(conn: &mut MysqlConnection, tblid: i32, rowpos: i32) -> Result { + + jrentries + .filter(jrtable_id.eq(tblid)) + .filter(row_pos.eq(rowpos)) + .select(id) + .first::(conn) + +} + +pub fn get_entries_of(conn: &mut MysqlConnection, tblid: i32) -> Result, diesel::result::Error> { + jrentries + .filter(jrtable_id.eq(tblid)) + .select(Jrentry::as_select()) + .load(conn) +} diff --git a/inventur_db/src/jrtables.rs b/inventur_db/src/jrtables.rs new file mode 100644 index 0000000..f9f3b70 --- /dev/null +++ b/inventur_db/src/jrtables.rs @@ -0,0 +1,95 @@ +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; +use crate::models; +use crate::schema; +use crate::jrcolumns; +use models::{ Jrtable, NewJrtable }; +use schema::jrtables::dsl::{ jrtables, id, name, owner_id }; + +pub fn create_jrtable(conn: &mut MysqlConnection, tblname: &String, field_names: Vec, uid: i32) -> Result { + let jrtable = NewJrtable { name: tblname.to_string(), owner_id: uid }; + + let tbl = diesel::insert_into(crate::schema::jrtables::table) + .values(&jrtable) + .execute(conn); + + if tbl.is_err() { + return Err(tbl.err().unwrap()); + } + let tblid = jrtables + .order(id.desc()) + .select(id) + .first::(conn); + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + let tblid = tblid.unwrap(); + let cols = jrcolumns::create_jrcolumns_empty_tbl(conn, tblid, field_names); + if cols.is_err() { + return Err(cols.err().unwrap()); + } + Ok(tblid) + +} + +pub fn rename_jrtable(conn: &mut MysqlConnection, tblid: i32, new_name: &str) -> Result { + diesel::update(jrtables.find(tblid)) + .set(name.eq(new_name)) + .execute(conn) +} + +pub fn delete_jrtable(conn: &mut MysqlConnection, tblid: i32) -> Result { + diesel::delete(jrtables.find(tblid)) + .execute(conn) +} + +pub fn get_all_tables(conn: &mut MysqlConnection) -> Result, diesel::result::Error> { + jrtables + .select(Jrtable::as_select()) + .load(conn) +} + +pub fn get_tbl(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrtables + .find(tblid) + .select(Jrtable::as_select()) + .first(conn) +} + +pub fn get_ncols(conn: &mut MysqlConnection, tblid: i32) -> Result { + use schema::jrcolumns::dsl::{ jrcolumns, jrtable_id }; + + jrcolumns + .filter(jrtable_id.eq(tblid)) + .execute(conn) +} + +pub fn get_nrows(conn: &mut MysqlConnection, tblid: i32) -> Result { + use schema::jrentries::dsl::{jrentries, jrtable_id}; + + jrentries + .filter(jrtable_id.eq(tblid)) + .execute(conn) +} + +pub fn get_tblids_uid(conn: &mut MysqlConnection, uid: i32) -> Result, diesel::result::Error> { + jrtables + .filter(owner_id.eq(uid)) + .select(id) + .load::(conn) +} + +pub fn get_owner_id(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrtables + .find(tblid) + .select(owner_id) + .first::(conn) +} + +pub fn get_tbl_by_name_uid(conn: &mut MysqlConnection, tblname: &String, uid: i32) -> Result { + jrtables + .filter(owner_id.eq(uid)) + .filter(name.eq(tblname)) + .select(id) + .first::(conn) +} diff --git a/inventur_db/src/jrtables.rs~ b/inventur_db/src/jrtables.rs~ new file mode 100644 index 0000000..f9f3b70 --- /dev/null +++ b/inventur_db/src/jrtables.rs~ @@ -0,0 +1,95 @@ +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; +use crate::models; +use crate::schema; +use crate::jrcolumns; +use models::{ Jrtable, NewJrtable }; +use schema::jrtables::dsl::{ jrtables, id, name, owner_id }; + +pub fn create_jrtable(conn: &mut MysqlConnection, tblname: &String, field_names: Vec, uid: i32) -> Result { + let jrtable = NewJrtable { name: tblname.to_string(), owner_id: uid }; + + let tbl = diesel::insert_into(crate::schema::jrtables::table) + .values(&jrtable) + .execute(conn); + + if tbl.is_err() { + return Err(tbl.err().unwrap()); + } + let tblid = jrtables + .order(id.desc()) + .select(id) + .first::(conn); + if tblid.is_err() { + return Err(tblid.err().unwrap()); + } + let tblid = tblid.unwrap(); + let cols = jrcolumns::create_jrcolumns_empty_tbl(conn, tblid, field_names); + if cols.is_err() { + return Err(cols.err().unwrap()); + } + Ok(tblid) + +} + +pub fn rename_jrtable(conn: &mut MysqlConnection, tblid: i32, new_name: &str) -> Result { + diesel::update(jrtables.find(tblid)) + .set(name.eq(new_name)) + .execute(conn) +} + +pub fn delete_jrtable(conn: &mut MysqlConnection, tblid: i32) -> Result { + diesel::delete(jrtables.find(tblid)) + .execute(conn) +} + +pub fn get_all_tables(conn: &mut MysqlConnection) -> Result, diesel::result::Error> { + jrtables + .select(Jrtable::as_select()) + .load(conn) +} + +pub fn get_tbl(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrtables + .find(tblid) + .select(Jrtable::as_select()) + .first(conn) +} + +pub fn get_ncols(conn: &mut MysqlConnection, tblid: i32) -> Result { + use schema::jrcolumns::dsl::{ jrcolumns, jrtable_id }; + + jrcolumns + .filter(jrtable_id.eq(tblid)) + .execute(conn) +} + +pub fn get_nrows(conn: &mut MysqlConnection, tblid: i32) -> Result { + use schema::jrentries::dsl::{jrentries, jrtable_id}; + + jrentries + .filter(jrtable_id.eq(tblid)) + .execute(conn) +} + +pub fn get_tblids_uid(conn: &mut MysqlConnection, uid: i32) -> Result, diesel::result::Error> { + jrtables + .filter(owner_id.eq(uid)) + .select(id) + .load::(conn) +} + +pub fn get_owner_id(conn: &mut MysqlConnection, tblid: i32) -> Result { + jrtables + .find(tblid) + .select(owner_id) + .first::(conn) +} + +pub fn get_tbl_by_name_uid(conn: &mut MysqlConnection, tblname: &String, uid: i32) -> Result { + jrtables + .filter(owner_id.eq(uid)) + .filter(name.eq(tblname)) + .select(id) + .first::(conn) +} diff --git a/inventur_db/src/lib.rs b/inventur_db/src/lib.rs new file mode 100644 index 0000000..2ab8e34 --- /dev/null +++ b/inventur_db/src/lib.rs @@ -0,0 +1,388 @@ +//! Database API using the table style system provided by this crate +mod models; +mod schema; +mod jrtables; +mod jrcolumns; +mod jrentries; +mod jrcells; +mod users; + +use dotenvy::dotenv; +use std::env; +use diesel::mysql::MysqlConnection; +use diesel::prelude::*; +use std::hash::{Hash, DefaultHasher}; +use std::collections::HashSet; +use diesel::deserialize::FromSql; +use diesel::deserialize; +use diesel::backend::Backend; +use diesel::sql_types::Integer; + +#[derive(PartialEq, Clone, Copy, diesel::FromSqlRow)] +#[repr(i32)] +pub enum FIELDTYPE { + TEXT = 0, + NUMBER = 1, +} + +impl FromSql for FIELDTYPE +where DB: Backend, i32: FromSql { + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + match i32::from_sql(bytes)? { + 0 => Ok(FIELDTYPE::TEXT), + 1 => Ok(FIELDTYPE::NUMBER), + x => Err(format!("Unknown field type {}.", x).into()), + } + } +} + +/// represents and summarised all relevant data of a table. +/// Standard return type if whole tables should be returned +pub struct Tbl { + /// table id of the represented object + pub tblid: i32, + pub name: String, + pub column_names: Vec, + pub column_types: Vec, + pub rows: Vec, +} + +/// Represents a table's row. +/// Internal data structure is abstracted until strings. +#[derive(Hash, Clone, Debug)] +pub struct TRow { + pub row_pos: i32, + pub cells: Vec, +} + +impl PartialEq for TRow { + fn eq(&self, other: &Self) -> bool { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher) == other.hash(&mut hasher) + } +} +impl Eq for TRow {} + +/// Identifies a user +/// All fields are unique within the db at any given point. +pub struct User { + /// User's unique id. + pub uid: i32, + pub uname: String, + pub email: String, +} + +/// Connect to database. +/// Return value must be passed to most of the crate's functions. +pub fn establish_connection() -> MysqlConnection { + dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("No database url set."); + MysqlConnection::establish(&db_url) + .unwrap_or_else(|_| panic!("Couldn't open database.")) +} + +/// Fetch one whole table from the database. Identified by it's ID. +/// Needs the id of the requesting user. +/// Returns: +/// A Tbl struct +/// Or: +/// None +/// if the table was not found or +/// if it could not be read or +/// if the requesting user is not the table's owner or +/// if there has been another error +/// +pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { + 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; + } + let clmns = clmns.unwrap(); + let mut clmn_nms = Vec::new(); + for clmn in &clmns { + 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); + } + let clmn_tps = clmn_tps; + let rowids = jrentries::get_entries_of(conn, tblid); + if rowids.is_err() { + return None; + } + let rowids = rowids.unwrap(); + let mut rows = Vec::new(); + for rowid in rowids { + let row = jrcells::get_entry_cells(conn, rowid.id); + if row.is_err() { + return None; + } + let row = row.unwrap(); + let mut data = Vec::new(); + for cell in row { + data.push(cell.cell_value); + } + 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 }) +} + +pub fn sort_table(tbl: Tbl, sort_field: usize, sort_dir: u8) -> Tbl { + let mut rows = tbl.rows; + if sort_field == 0 { + rows.sort_unstable_by_key(|a: &TRow| a.row_pos); + if sort_dir == 1 { + rows.reverse(); + } + }else { + if tbl.column_types[sort_field-1] == FIELDTYPE::NUMBER { + rows.sort_by_key(|a: &TRow| a.cells[sort_field - 1].trim().parse::().expect("Number not a number.")); + }else { + rows.sort_by_key(|a: &TRow| a.cells[sort_field - 1].clone()); + } + if sort_dir == 1 { + rows.reverse(); + } + } + let rows = rows; + Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: rows.clone() } +} + +pub fn search_table(tbl: Tbl, search_fields: Vec, search_value: String) -> Tbl { + let mut rows = tbl.rows; + let mut field_sets = HashSet::new(); + for field in search_fields { + for row in &rows { + if row.cells[field as usize].contains(&search_value) { + field_sets.insert(row.clone()); + } + } + } + Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: Vec::from_iter(field_sets) } +} + + +pub fn get_tblnames(conn: &mut MysqlConnection, tblids: Vec) -> Option> { + let mut tblnames = Vec::new(); + for tblid in tblids { + let tblname = jrtables::get_tbl(conn, tblid); + if tblname.is_err() { + return None; + } + tblnames.push(tblname.unwrap().name); + } + Some(tblnames) +} + +/// Returns a Vec of the ids of a user's table if the user exists, else None. +/// User is identified by its user ID. +pub fn get_user_tblids(conn: &mut MysqlConnection, uid: i32) -> Option> { + let tblids = jrtables::get_tblids_uid(conn, uid); + if tblids.is_err() { + return None; + } + Some(tblids.unwrap()) +} + +/// Change the name of a table. +/// Needs the user ID of the requesting user. +/// Returns Some(true) if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn rename_table(conn: &mut MysqlConnection, tblid: i32, new_name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid { + return None; + } + let rnm = jrtables::rename_jrtable(conn, tblid, &new_name); + if rnm.is_err() { + return None; + } + Some(true) +} + + +/// Delete a table. +/// Needs the user ID of the requesting user. +/// Returns Some(true) if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn delete_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid { + return None; + } + let dlt = jrtables::delete_jrtable(conn, tblid); + if dlt.is_err() { + return None; + } + Some(true) +} + + +/// Create a new table. +/// Needs the user ID of the requesting user. +/// Returns the id of the newly created table if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn create_table(conn: &mut MysqlConnection, name: String, field_names: Vec, uid: i32) -> Option { + if jrtables::get_tbl_by_name_uid(conn, &name, uid).is_ok() { + return None; + } + if jrtables::create_jrtable(conn, &name, field_names, uid).is_err() { + return None; + } + let tblid = jrtables::get_tbl_by_name_uid(conn, &name, uid); + if tblid.is_err() { + return None; + } + Some(tblid.unwrap()) +} + +/// Returns a User struct of a requested user if the combination of username and email are +/// consistent or both do not exist yet, in which case the user is newly created. +pub fn register_or_login(conn: &mut MysqlConnection, uname: String, mail: String) -> Option { + let mut uid = users::get_uid_email(conn, &mail); + if uid.is_err() { + if users::get_uid_uname(conn, &uname).is_ok() || users::create_user(conn, &uname, &mail).is_err() { + return None; + } + uid = users::get_uid_email(conn, &mail); + if uid.is_err() { + return None; + } + } + let uid = uid.unwrap(); + let nm = users::get_uname(conn, uid); + if nm.is_err() || nm.unwrap() != uname { + return None; + } + Some(User { uid: uid, uname: uname, email: mail }) +} + +pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Option { + if users::delete_user(conn, uid).is_err() { + return None; + } + Some(true) +} + +pub fn add_row(conn: &mut MysqlConnection, tblid: i32, values: Vec, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + let nrows = jrtables::get_nrows(conn, tblid); + if nrows.is_err() || owner.is_err() || owner.unwrap() != uid { + return None; + } + let nrows = nrows.unwrap() as i32; + if jrentries::create_jrentry(conn, tblid).is_err() { + return None; + } + let entryid = jrentries::get_jrentry_id(conn, tblid, nrows+1); + if entryid.is_err() { + return None; + } + let entryid = entryid.unwrap(); + let cols = jrcolumns::get_clmns_of(conn, tblid); + if cols.is_err() { + return None; + } + let cols = cols.unwrap(); + for (i,col) in cols.iter().enumerate() { + if jrcells::create_jrcell(conn, entryid, col.id, &values[i]).is_err() { + return None; + } + } + Some(entryid) +} + +pub fn delete_row(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, uid: i32) -> Option { + 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() { + return None; + } + Some(true) +} + +pub fn move_row(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newpos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid || jrentries::move_jrentry(conn, tblid, rowpos, newpos).is_err() { + return None; + } + Some(true) +} + + +pub fn edit_cell(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, column_pos: i32, new_value: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcells::change_jrcell_value_relative(conn, tblid, row_pos, column_pos, new_value).is_err() + { + return None; + } + Some(true) +} + +pub fn move_column(conn: &mut MysqlConnection, tblid:i32, column_pos: i32, new_column_pos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::move_column_relative(conn, tblid, column_pos, new_column_pos).is_err() + { + return None; + } + Some(true) +} + +pub fn add_column(conn: &mut MysqlConnection, tblid: i32, name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::create_jrcolumn(conn, tblid, name).is_err() + { + return None; + } + let clmid = jrcolumns::get_last_column_id_of(conn, tblid); + if clmid.is_err() { + return None; + } + Some(clmid.unwrap()) +} + +pub fn delete_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::delete_column_relative(conn, tblid, column_pos).is_err() + { + return None; + } + Some(true) +} + +pub fn rename_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::rename_column_relative(conn, tblid, column_pos, new_name).is_err() + { + return None; + } + Some(true) +} diff --git a/inventur_db/src/lib.rs~ b/inventur_db/src/lib.rs~ new file mode 100644 index 0000000..b7f35a4 --- /dev/null +++ b/inventur_db/src/lib.rs~ @@ -0,0 +1,314 @@ +//! Database API using the table style system provided by this crate +mod models; +mod schema; +mod jrtables; +mod jrcolumns; +mod jrentries; +mod jrcells; +mod users; + +use dotenvy::dotenv; +use std::env; +use diesel::mysql::MysqlConnection; +use diesel::prelude::*; + +/// represents and summarised all relevant data of a table. +/// Standard return type if whole tables should be returned +pub struct Tbl { + /// table id of the represented object + pub tblid: i32, + pub name: String, + pub column_names: Vec, + pub rows: Vec, +} + +/// Represents a table's row. +/// Internal data structure is abstracted until strings. +pub struct TRow { + pub row_pos: i32, + pub cells: Vec, +} + +/// Identifies a user +/// All fields are unique within the db at any given point. +pub struct User { + /// User's unique id. + pub uid: i32, + pub uname: String, + pub email: String, +} + +/// Connect to database. +/// Return value must be passed to most of the crate's functions. +pub fn establish_connection() -> MysqlConnection { + dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("No database url set."); + MysqlConnection::establish(&db_url) + .unwrap_or_else(|_| panic!("Couldn't open database.")) +} + +/// Fetch one whole table from the database. Identified by it's ID. +/// Needs the id of the requesting user. +/// Returns: +/// A Tbl struct +/// Or: +/// None +/// if the table was not found or +/// if it could not be read or +/// if the requesting user is not the table's owner or +/// if there has been another error +/// +pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { + 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; + } + let clmns = clmns.unwrap(); + let mut clmn_nms = Vec::new(); + for clmn in &clmns { + 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 rowids = jrentries::get_entries_of(conn, tblid); + if rowids.is_err() { + return None; + } + let rowids = rowids.unwrap(); + let mut rows = Vec::new(); + for rowid in rowids { + let row = jrcells::get_entry_cells(conn, rowid.id); + if row.is_err() { + return None; + } + let row = row.unwrap(); + let mut data = Vec::new(); + for cell in row { + data.push(cell.cell_value); + } + 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, rows: rows }) +} + +pub fn get_tblnames(conn: &mut MysqlConnection, tblids: Vec) -> Option> { + let mut tblnames = Vec::new(); + for tblid in tblids { + let tblname = jrtables::get_tbl(conn, tblid); + if tblname.is_err() { + return None; + } + tblnames.push(tblname.unwrap().name); + } + Some(tblnames) +} + +/// Returns a Vec of the ids of a user's table if the user exists, else None. +/// User is identified by its user ID. +pub fn get_user_tblids(conn: &mut MysqlConnection, uid: i32) -> Option> { + let tblids = jrtables::get_tblids_uid(conn, uid); + if tblids.is_err() { + return None; + } + Some(tblids.unwrap()) +} + +/// Change the name of a table. +/// Needs the user ID of the requesting user. +/// Returns Some(true) if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn rename_table(conn: &mut MysqlConnection, tblid: i32, new_name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid { + return None; + } + let rnm = jrtables::rename_jrtable(conn, tblid, &new_name); + if rnm.is_err() { + return None; + } + Some(true) +} + + +/// Delete a table. +/// Needs the user ID of the requesting user. +/// Returns Some(true) if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn delete_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid { + return None; + } + let dlt = jrtables::delete_jrtable(conn, tblid); + if dlt.is_err() { + return None; + } + Some(true) +} + + +/// Create a new table. +/// Needs the user ID of the requesting user. +/// Returns the id of the newly created table if the action seems to have been successful and the requesting user is the +/// table's owner. +/// None otherwise. +pub fn create_table(conn: &mut MysqlConnection, name: String, field_names: Vec, uid: i32) -> Option { + if jrtables::get_tbl_by_name_uid(conn, &name, uid).is_ok() { + return None; + } + if jrtables::create_jrtable(conn, &name, field_names, uid).is_err() { + return None; + } + let tblid = jrtables::get_tbl_by_name_uid(conn, &name, uid); + if tblid.is_err() { + return None; + } + Some(tblid.unwrap()) +} + +/// Returns a User struct of a requested user if the combination of username and email are +/// consistent or both do not exist yet, in which case the user is newly created. +pub fn register_or_login(conn: &mut MysqlConnection, uname: String, mail: String) -> Option { + let mut uid = users::get_uid_email(conn, &mail); + if uid.is_err() { + if users::get_uid_uname(conn, &uname).is_ok() || users::create_user(conn, &uname, &mail).is_err() { + return None; + } + uid = users::get_uid_email(conn, &mail); + if uid.is_err() { + return None; + } + } + let uid = uid.unwrap(); + let nm = users::get_uname(conn, uid); + if nm.is_err() || nm.unwrap() != uname { + return None; + } + Some(User { uid: uid, uname: uname, email: mail }) +} + +pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Option { + if users::delete_user(conn, uid).is_err() { + return None; + } + Some(true) +} + +pub fn add_row(conn: &mut MysqlConnection, tblid: i32, values: Vec, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + let nrows = jrtables::get_nrows(conn, tblid); + if nrows.is_err() || owner.is_err() || owner.unwrap() != uid { + return None; + } + let nrows = nrows.unwrap() as i32; + if jrentries::create_jrentry(conn, tblid).is_err() { + return None; + } + let entryid = jrentries::get_jrentry_id(conn, tblid, nrows+1); + if entryid.is_err() { + return None; + } + let entryid = entryid.unwrap(); + let cols = jrcolumns::get_clmns_of(conn, tblid); + if cols.is_err() { + return None; + } + let cols = cols.unwrap(); + for (i,col) in cols.iter().enumerate() { + if jrcells::create_jrcell(conn, entryid, col.id, &values[i]).is_err() { + return None; + } + } + Some(entryid) +} + +pub fn delete_row(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, uid: i32) -> Option { + 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() { + return None; + } + Some(true) +} + +pub fn move_row(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newpos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || owner.unwrap() != uid || jrentries::move_jrentry(conn, tblid, rowpos, newpos).is_err() { + return None; + } + Some(true) +} + + +pub fn edit_cell(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, column_pos: i32, new_value: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcells::change_jrcell_value_relative(conn, tblid, row_pos, column_pos, new_value).is_err() + { + return None; + } + Some(true) +} + +pub fn move_column(conn: &mut MysqlConnection, tblid:i32, column_pos: i32, new_column_pos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::move_column_relative(conn, tblid, column_pos, new_column_pos).is_err() + { + return None; + } + Some(true) +} + +pub fn add_column(conn: &mut MysqlConnection, tblid: i32, name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::create_jrcolumn(conn, tblid, name).is_err() + { + return None; + } + let clmid = jrcolumns::get_last_column_id_of(conn, tblid); + if clmid.is_err() { + return None; + } + Some(clmid.unwrap()) +} + +pub fn delete_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::delete_column_relative(conn, tblid, column_pos).is_err() + { + return None; + } + Some(true) +} + +pub fn rename_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_name: String, uid: i32) -> Option { + let owner = jrtables::get_owner_id(conn, tblid); + if owner.is_err() || + owner.unwrap() != uid || + jrcolumns::rename_column_relative(conn, tblid, column_pos, new_name).is_err() + { + return None; + } + Some(true) +} diff --git a/inventur_db/src/models.rs b/inventur_db/src/models.rs new file mode 100644 index 0000000..7216264 --- /dev/null +++ b/inventur_db/src/models.rs @@ -0,0 +1,98 @@ +use diesel::prelude::*; +use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users}; + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = crate::schema::jrtables)] +#[diesel(belongs_to(User, foreign_key = owner_id))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrtable { + pub id: i32, + pub name: String, + pub owner_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrtables)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrtable { + pub name: String, + pub owner_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrcolumns)] +#[diesel(belongs_to(Jrtable))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrcolumn { + pub id: i32, + pub column_type: crate::FIELDTYPE, + pub name: String, + pub jrtable_id: i32, + pub id_in_table: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrcolumns)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrcolumn { + pub name: String, + pub jrtable_id: i32, + pub id_in_table: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrentries)] +#[diesel(belongs_to(Jrtable))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrentry { + pub id: i32, + pub row_pos: i32, + pub jrtable_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrentries)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrentry { + pub row_pos: i32, + pub jrtable_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrcells)] +#[diesel(belongs_to(Jrentry))] +#[diesel(belongs_to(Jrcolumn))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrcell { + pub id: i32, + pub cell_value: String, + pub jrentry_id: i32, + pub jrcolumn_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrcells)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrcell { + pub cell_value: String, + pub jrentry_id: i32, + pub jrcolumn_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct User { + pub id: i32, + pub username: String, + pub email: String, +} + +#[derive(Insertable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewUser { + pub username: String, + pub email: String, +} + diff --git a/inventur_db/src/models.rs~ b/inventur_db/src/models.rs~ new file mode 100644 index 0000000..c90e59b --- /dev/null +++ b/inventur_db/src/models.rs~ @@ -0,0 +1,97 @@ +use diesel::prelude::*; +use crate::schema::{jrtables, jrcolumns, jrentries, jrcells, users}; + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = crate::schema::jrtables)] +#[diesel(belongs_to(User, foreign_key = owner_id))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrtable { + pub id: i32, + pub name: String, + pub owner_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrtables)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrtable { + pub name: String, + pub owner_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrcolumns)] +#[diesel(belongs_to(Jrtable))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrcolumn { + pub id: i32, + pub name: String, + pub jrtable_id: i32, + pub id_in_table: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrcolumns)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrcolumn { + pub name: String, + pub jrtable_id: i32, + pub id_in_table: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrentries)] +#[diesel(belongs_to(Jrtable))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrentry { + pub id: i32, + pub row_pos: i32, + pub jrtable_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrentries)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrentry { + pub row_pos: i32, + pub jrtable_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = jrcells)] +#[diesel(belongs_to(Jrentry))] +#[diesel(belongs_to(Jrcolumn))] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct Jrcell { + pub id: i32, + pub cell_value: String, + pub jrentry_id: i32, + pub jrcolumn_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = jrcells)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewJrcell { + pub cell_value: String, + pub jrentry_id: i32, + pub jrcolumn_id: i32, +} + +#[derive(Queryable, Selectable, Identifiable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct User { + pub id: i32, + pub username: String, + pub email: String, +} + +#[derive(Insertable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct NewUser { + pub username: String, + pub email: String, +} + diff --git a/inventur_db/src/schema.rs b/inventur_db/src/schema.rs new file mode 100644 index 0000000..4596166 --- /dev/null +++ b/inventur_db/src/schema.rs @@ -0,0 +1,63 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + jrcells (id) { + id -> Integer, + #[max_length = 2048] + cell_value -> Varchar, + jrentry_id -> Integer, + jrcolumn_id -> Integer, + } +} + +diesel::table! { + jrcolumns (id) { + id -> Integer, + #[max_length = 255] + name -> Varchar, + jrtable_id -> Integer, + id_in_table -> Integer, + column_type -> Integer, + } +} + +diesel::table! { + jrentries (id) { + id -> Integer, + row_pos -> Integer, + jrtable_id -> Integer, + } +} + +diesel::table! { + jrtables (id) { + id -> Integer, + #[max_length = 255] + name -> Varchar, + owner_id -> Integer, + } +} + +diesel::table! { + users (id) { + id -> Integer, + #[max_length = 255] + username -> Varchar, + #[max_length = 512] + email -> Varchar, + } +} + +diesel::joinable!(jrcells -> jrcolumns (jrcolumn_id)); +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::allow_tables_to_appear_in_same_query!( + jrcells, + jrcolumns, + jrentries, + jrtables, + users, +); diff --git a/inventur_db/src/users.rs b/inventur_db/src/users.rs new file mode 100644 index 0000000..a6b9514 --- /dev/null +++ b/inventur_db/src/users.rs @@ -0,0 +1,56 @@ +use crate::models; +use crate::schema; + +use schema::users::dsl::{users, id, username, email}; +use models::NewUser; + +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; + +pub fn create_user(conn: &mut MysqlConnection, uname: &String, mail: &String) -> Result { + let user = NewUser { username: uname.to_string(), email: mail.to_string()}; + diesel::insert_into(crate::schema::users::table) + .values(&user) + .execute(conn) +} + +pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Result { + diesel::delete(users.find(uid)) + .execute(conn) +} + +pub fn get_uid_email(conn: &mut MysqlConnection, mail: &String) -> Result { + users + .filter(email.eq(mail)) + .select(id) + .first::(conn) +} + +pub fn get_uid_uname(conn: &mut MysqlConnection, uname: &String) -> Result { + users + .filter(username.eq(uname)) + .select(id) + .first::(conn) +} + +pub fn get_uname(conn: &mut MysqlConnection, uid: i32) -> Result { + users + .find(uid) + .select(username) + .first::(conn) +} + +pub fn get_email(conn: &mut MysqlConnection, uid: i32) -> Result { + users + .find(uid) + .select(email) + .first::(conn) +} + +pub fn get_user_tables(conn: &mut MysqlConnection, uid: i32) -> Result, diesel::result::Error> { + use schema::jrtables::dsl::{jrtables, id as tblid, owner_id}; + jrtables + .filter(owner_id.eq(uid)) + .select(tblid) + .load::(conn) +} diff --git a/inventur_db/src/users.rs~ b/inventur_db/src/users.rs~ new file mode 100644 index 0000000..4d25808 --- /dev/null +++ b/inventur_db/src/users.rs~ @@ -0,0 +1,56 @@ +use crate::models; +use crate::schema; + +use schema::users::dsl::{users, id, username, email}; +use models::NewUser; + +use diesel::prelude::*; +use diesel::mysql::MysqlConnection; + +pub fn create_user(conn: &mut MysqlConnection, uname: String, mail: String) -> Result { + let user = NewUser { username: uname, email: mail}; + diesel::insert_into(crate::schema::users::table) + .values(&user) + .execute(conn) +} + +pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Result { + diesel::delete(users.find(uid)) + .execute(conn) +} + +pub fn get_uid_email(conn: &mut MysqlConnection, mail: String) -> Result { + users + .filter(email.eq(mail)) + .select(id) + .first::(conn) +} + +pub fn get_uid_uname(conn: &mut MysqlConnection, uname: String) -> Result { + users + .filter(username.eq(uname)) + .select(id) + .first::(conn) +} + +pub fn get_uname(conn: &mut MysqlConnection, uid: i32) -> Result { + users + .find(uid) + .select(username) + .first::(conn) +} + +pub fn get_email(conn: &mut MysqlConnection, uid: i32) -> Result { + users + .find(uid) + .select(email) + .first::(conn) +} + +pub fn get_user_tables(conn: &mut MysqlConnection, uid: i32) -> Result, diesel::result::Error> { + use schema::jrtables::dsl::{jrtables, id as tblid, owner_id}; + jrtables + .filter(owner_id.eq(uid)) + .select(tblid) + .load::(conn) +} diff --git a/migrations/2024-08-09-133227_create_users/up.sql b/migrations/2024-08-09-133227_create_users/up.sql deleted file mode 100644 index 1e2245d..0000000 --- a/migrations/2024-08-09-133227_create_users/up.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Your SQL goes here -CREATE TABLE users ( - id INT PRIMARY KEY, - email VARCHAR(64) NOT NULL, - name VARCHAR(255) -); diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..03fbb6f --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "inventur", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + } + } +} diff --git a/node_modules/papaparse/.eslintrc.js b/node_modules/papaparse/.eslintrc.js new file mode 100644 index 0000000..c3ed7c5 --- /dev/null +++ b/node_modules/papaparse/.eslintrc.js @@ -0,0 +1,279 @@ +module.exports = { + "parserOptions": { + "ecmaVersion": 5 + }, + "env": { + "browser": true, + "worker": true, + "node": true + }, + "extends": "eslint:recommended", + "rules": { + "accessor-pairs": "error", + "array-bracket-newline": ["error", "consistent"], + "array-bracket-spacing": [ + "error", + "never" + ], + "array-callback-return": "error", + "array-element-newline": "off", + "arrow-body-style": "error", + "arrow-parens": "error", + "arrow-spacing": "error", + "block-scoped-var": "error", + "block-spacing": "error", + "brace-style": "off", + "callback-return": "error", + "camelcase": ["error", {"properties": "never"}], + "capitalized-comments": "off", + "class-methods-use-this": "error", + "comma-dangle": "off", + "comma-spacing": "off", + "comma-style": [ + "error", + "last" + ], + "complexity": "off", + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-return": "off", + "consistent-this": "off", + "curly": "off", + "default-case": "error", + "dot-location": "error", + "dot-notation": "error", + "eol-last": "error", + "eqeqeq": "error", + "for-direction": "error", + "func-call-spacing": "error", + "func-name-matching": "error", + "func-names": [ + "error", + "never" + ], + "func-style": "off", + "function-paren-newline": "off", + "generator-star-spacing": "error", + "getter-return": "error", + "global-require": "off", + "guard-for-in": "off", + "handle-callback-err": "error", + "id-blacklist": "error", + "id-length": "off", + "id-match": "error", + "implicit-arrow-linebreak": "error", + "indent": [ + "error", + "tab" + ], + "indent-legacy": "off", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "error", + "keyword-spacing": "off", + "line-comment-position": "off", + "linebreak-style": [ + "error", + "unix" + ], + "lines-around-comment": "off", + "lines-around-directive": "off", + "lines-between-class-members": "error", + "max-depth": "off", + "max-len": "off", + "max-lines": "off", + "max-nested-callbacks": "error", + "max-params": "off", + "max-statements": "off", + "max-statements-per-line": "off", + "multiline-comment-style": "off", + "multiline-ternary": [ + "error", + "always-multiline" + ], + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "off", + "newline-per-chained-call": "off", + "no-alert": "error", + "no-array-constructor": "error", + "no-await-in-loop": "error", + "no-bitwise": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-catch-shadow": "off", + "no-cond-assign": [ + "error", + "except-parens" + ], + "no-confusing-arrow": "error", + "no-console": "off", + "no-continue": "off", + "no-div-regex": "error", + "no-duplicate-imports": "error", + "no-else-return": "off", + "no-empty": ["error", {"allowEmptyCatch": true}], + "no-empty-function": "off", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-label": "error", + "no-extra-parens": "off", + "no-floating-decimal": "error", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-inline-comments": "off", + "no-inner-declarations": [ + "error", + "functions" + ], + "no-invalid-this": "off", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-magic-numbers": "off", + "no-mixed-operators": "off", + "no-mixed-requires": "error", + "no-multi-assign": "error", + "no-multi-spaces": "off", + "no-multi-str": "error", + "no-multiple-empty-lines": "off", + "no-native-reassign": "error", + "no-negated-condition": "off", + "no-negated-in-lhs": "error", + "no-nested-ternary": "off", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "off", + "no-path-concat": "off", + "no-plusplus": "off", + "no-process-env": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-restricted-globals": "error", + "no-restricted-imports": "error", + "no-restricted-modules": "error", + "no-restricted-properties": "error", + "no-restricted-syntax": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "off", + "no-shadow-restricted-names": "error", + "no-spaced-func": "error", + "no-sync": ["error", {"allowAtRootLevel": true}], + "no-tabs": "off", + "no-template-curly-in-string": "error", + "no-ternary": "off", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef-init": "error", + "no-undefined": "off", + "no-underscore-dangle": "off", + "no-unmodified-loop-condition": "off", + "no-unneeded-ternary": "error", + "no-unused-expressions": "off", + "no-unused-vars": ["error", {"args": "none"}], + "no-use-before-define": "off", + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "off", + "no-var": "off", + "no-void": "error", + "no-warning-comments": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "nonblock-statement-body-position": [ + "error", + "any" + ], + "object-curly-newline": ["error", {"consistent": true}], + "object-curly-spacing": "off", + "object-shorthand": "off", + "one-var": "off", + "one-var-declaration-per-line": "off", + "operator-assignment": [ + "error", + "always" + ], + "operator-linebreak": "off", + "padded-blocks": "off", + "padding-line-between-statements": "error", + "prefer-arrow-callback": "off", + "prefer-const": "error", + "prefer-destructuring": "off", + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-reflect": "off", + "prefer-rest-params": "off", + "prefer-spread": "error", + "prefer-template": "off", + "quote-props": "off", + "quotes": "off", + "radix": [ + "error", + "as-needed" + ], + "require-await": "error", + "require-jsdoc": "off", + "rest-spread-spacing": "error", + "semi": "error", + "semi-spacing": "error", + "semi-style": [ + "error", + "last" + ], + "sort-imports": "error", + "sort-keys": "off", + "sort-vars": "off", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + "never" + ], + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + "nonwords": false, + "words": false + } + ], + "spaced-comment": "off", + "strict": "off", + "switch-colon-spacing": "error", + "symbol-description": "error", + "template-curly-spacing": "error", + "template-tag-spacing": "error", + "unicode-bom": [ + "error", + "never" + ], + "valid-jsdoc": "off", + "vars-on-top": "off", + "wrap-iife": "off", + "wrap-regex": "off", + "yield-star-spacing": "error", + "yoda": "off" + } +}; diff --git a/node_modules/papaparse/.github/workflows/node.js.yml b/node_modules/papaparse/.github/workflows/node.js.yml new file mode 100644 index 0000000..0fccdf0 --- /dev/null +++ b/node_modules/papaparse/.github/workflows/node.js.yml @@ -0,0 +1,29 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/node_modules/papaparse/Gruntfile.js b/node_modules/papaparse/Gruntfile.js new file mode 100644 index 0000000..93b1256 --- /dev/null +++ b/node_modules/papaparse/Gruntfile.js @@ -0,0 +1,27 @@ +module.exports = function(grunt) { + grunt.initConfig({ + uglify: { + options: { + compress: { + global_defs: { + 'PAPA_BROWSER_CONTEXT': true + }, + dead_code: true + }, + output: { + comments: 'some', + }, + }, + min: { + files: { + 'papaparse.min.js': ['papaparse.js'] + }, + }, + }, + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + + grunt.registerTask('build', ['uglify']); + grunt.registerTask('default', ['uglify']); +}; diff --git a/node_modules/papaparse/LICENSE b/node_modules/papaparse/LICENSE new file mode 100644 index 0000000..12f5b35 --- /dev/null +++ b/node_modules/papaparse/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Matthew Holt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/papaparse/README.md b/node_modules/papaparse/README.md new file mode 100644 index 0000000..9af8ad8 --- /dev/null +++ b/node_modules/papaparse/README.md @@ -0,0 +1,67 @@ +Parse CSV with JavaScript +======================================== + +Papa Parse is the fastest in-browser CSV (or delimited text) parser for JavaScript. It is reliable and correct according to [RFC 4180](https://tools.ietf.org/html/rfc4180), and it comes with these features: + +- Easy to use +- Parse CSV files directly (local or over the network) +- Fast mode +- Stream large files (even via HTTP) +- Reverse parsing (converts JSON to CSV) +- Auto-detect delimiter +- Worker threads to keep your web page reactive +- Header row support +- Pause, resume, abort +- Can convert numbers and booleans to their types +- Optional jQuery integration to get files from `` elements +- One of the only parsers that correctly handles line-breaks and quotations + +Papa Parse has **no dependencies** - not even jQuery. + +Install +------- + +papaparse is available on [npm](https://www.npmjs.com/package/papaparse). It +can be installed with the following command: + + npm install papaparse + +If you don't want to use npm, [papaparse.min.js](https://unpkg.com/papaparse@latest/papaparse.min.js) can be downloaded to your project source. + + +Homepage & Demo +---------------- + +- [Homepage](http://papaparse.com) +- [Demo](http://papaparse.com/demo) + +To learn how to use Papa Parse: + +- [Documentation](http://papaparse.com/docs) + +The website is hosted on [Github Pages](https://pages.github.com/). Its content is also included in the docs folder of this repository. If you want to contribute on it just clone the master of this repository and open a pull request. + + +Papa Parse for Node +-------------------- + +Papa Parse can parse a [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams) instead of a [File](https://www.w3.org/TR/FileAPI/) when used in Node.js environments (in addition to plain strings). In this mode, `encoding` must, if specified, be a Node-supported character encoding. The `Papa.LocalChunkSize`, `Papa.RemoteChunkSize` , `download`, `withCredentials` and `worker` config options are unavailable. + +Papa Parse can also parse in a node streaming style which makes `.pipe` available. Simply pipe the [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams) to the stream returned from `Papa.parse(Papa.NODE_STREAM_INPUT, options)`. The `Papa.LocalChunkSize`, `Papa.RemoteChunkSize` , `download`, `withCredentials`, `worker`, `step`, and `complete` config options are unavailable. To register a callback with the stream to process data, use the `data` event like so: `stream.on('data', callback)` and to signal the end of stream, use the 'end' event like so: `stream.on('end', callback)`. + +Get Started +----------- + +For usage instructions, see the [homepage](http://papaparse.com) and, for more detail, the [documentation](http://papaparse.com/docs). + +Tests +----- + +Papa Parse is under test. Download this repository, run `npm install`, then `npm test` to run the tests. + +Contributing +------------ + +To discuss a new feature or ask a question, open an issue. To fix a bug, submit a pull request to be credited with the [contributors](https://github.com/mholt/PapaParse/graphs/contributors)! Remember, a pull request, *with test*, is best. You may also discuss on Twitter with [#PapaParse](https://twitter.com/search?q=%23PapaParse&src=typd&f=realtime) or directly to me, [@mholt6](https://twitter.com/mholt6). + +If you contribute a patch, ensure the tests suite is running correctly. We run continuous integration on each pull request and will not accept a patch that breaks the tests. diff --git a/node_modules/papaparse/bower.json b/node_modules/papaparse/bower.json new file mode 100644 index 0000000..62e9ab5 --- /dev/null +++ b/node_modules/papaparse/bower.json @@ -0,0 +1,40 @@ +{ + "name": "papaparse", + "main": "papaparse.js", + "homepage": "http://papaparse.com", + "authors": [ + "Matthew Holt" + ], + "description": "Fast and powerful CSV parser for the browser. Converts CSV->JSON and JSON->CSV. Supports web workers and streaming large files.", + "keywords": [ + "csv", + "parse", + "parsing", + "parser", + "delimited", + "text", + "data", + "auto-detect", + "comma", + "tab", + "pipe", + "file", + "filereader", + "stream", + "worker", + "workers", + "ajax", + "thread", + "threading", + "multi-threaded" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "player" + ] +} diff --git a/node_modules/papaparse/package.json b/node_modules/papaparse/package.json new file mode 100644 index 0000000..54f9952 --- /dev/null +++ b/node_modules/papaparse/package.json @@ -0,0 +1,57 @@ +{ + "name": "papaparse", + "version": "5.4.1", + "description": "Fast and powerful CSV parser for the browser that supports web workers and streaming large files. Converts CSV to JSON and JSON to CSV.", + "keywords": [ + "csv", + "parser", + "parse", + "parsing", + "delimited", + "text", + "data", + "auto-detect", + "comma", + "tab", + "pipe", + "file", + "filereader", + "stream", + "worker", + "workers", + "thread", + "threading", + "multi-threaded", + "jquery-plugin" + ], + "homepage": "http://papaparse.com", + "repository": { + "type": "git", + "url": "https://github.com/mholt/PapaParse.git" + }, + "author": { + "name": "Matthew Holt", + "url": "https://twitter.com/mholt6" + }, + "license": "MIT", + "main": "papaparse.js", + "browser": "papaparse.min.js", + "devDependencies": { + "chai": "^4.2.0", + "connect": "^3.3.3", + "eslint": "^4.19.1", + "grunt": "^1.0.2", + "grunt-contrib-uglify": "^3.3.0", + "mocha": "^5.2.0", + "mocha-headless-chrome": "^4.0.0", + "open": "7.0.0", + "serve-static": "^1.7.1" + }, + "scripts": { + "lint": "eslint --no-ignore papaparse.js Gruntfile.js .eslintrc.js 'tests/**/*.js'", + "test-browser": "node tests/test.js", + "test-mocha-headless-chrome": "node tests/test.js --mocha-headless-chrome", + "test-node": "mocha tests/node-tests.js tests/test-cases.js", + "test": "npm run lint && npm run test-node && npm run test-mocha-headless-chrome" + } +} diff --git a/node_modules/papaparse/papaparse.js b/node_modules/papaparse/papaparse.js new file mode 100755 index 0000000..d94773b --- /dev/null +++ b/node_modules/papaparse/papaparse.js @@ -0,0 +1,1922 @@ +/* @license +Papa Parse +v5.4.1 +https://github.com/mholt/PapaParse +License: MIT +*/ + +(function(root, factory) +{ + /* globals define */ + if (typeof define === 'function' && define.amd) + { + // AMD. Register as an anonymous module. + define([], factory); + } + else if (typeof module === 'object' && typeof exports !== 'undefined') + { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } + else + { + // Browser globals (root is window) + root.Papa = factory(); + } + // in strict mode we cannot access arguments.callee, so we need a named reference to + // stringify the factory method for the blob worker + // eslint-disable-next-line func-name +}(this, function moduleFactory() +{ + 'use strict'; + + var global = (function() { + // alternative method, similar to `Function('return this')()` + // but without using `eval` (which is disabled when + // using Content Security Policy). + + if (typeof self !== 'undefined') { return self; } + if (typeof window !== 'undefined') { return window; } + if (typeof global !== 'undefined') { return global; } + + // When running tests none of the above have been defined + return {}; + })(); + + + function getWorkerBlob() { + var URL = global.URL || global.webkitURL || null; + var code = moduleFactory.toString(); + return Papa.BLOB_URL || (Papa.BLOB_URL = URL.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ", '(', code, ')();'], {type: 'text/javascript'}))); + } + + var IS_WORKER = !global.document && !!global.postMessage, + IS_PAPA_WORKER = global.IS_PAPA_WORKER || false; + + var workers = {}, workerIdCounter = 0; + + var Papa = {}; + + Papa.parse = CsvToJson; + Papa.unparse = JsonToCsv; + + Papa.RECORD_SEP = String.fromCharCode(30); + Papa.UNIT_SEP = String.fromCharCode(31); + Papa.BYTE_ORDER_MARK = '\ufeff'; + Papa.BAD_DELIMITERS = ['\r', '\n', '"', Papa.BYTE_ORDER_MARK]; + Papa.WORKERS_SUPPORTED = !IS_WORKER && !!global.Worker; + Papa.NODE_STREAM_INPUT = 1; + + // Configurable chunk sizes for local and remote files, respectively + Papa.LocalChunkSize = 1024 * 1024 * 10; // 10 MB + Papa.RemoteChunkSize = 1024 * 1024 * 5; // 5 MB + Papa.DefaultDelimiter = ','; // Used if not specified and detection fails + + // Exposed for testing and development only + Papa.Parser = Parser; + Papa.ParserHandle = ParserHandle; + Papa.NetworkStreamer = NetworkStreamer; + Papa.FileStreamer = FileStreamer; + Papa.StringStreamer = StringStreamer; + Papa.ReadableStreamStreamer = ReadableStreamStreamer; + if (typeof PAPA_BROWSER_CONTEXT === 'undefined') { + Papa.DuplexStreamStreamer = DuplexStreamStreamer; + } + + if (global.jQuery) + { + var $ = global.jQuery; + $.fn.parse = function(options) + { + var config = options.config || {}; + var queue = []; + + this.each(function(idx) + { + var supported = $(this).prop('tagName').toUpperCase() === 'INPUT' + && $(this).attr('type').toLowerCase() === 'file' + && global.FileReader; + + if (!supported || !this.files || this.files.length === 0) + return true; // continue to next input element + + for (var i = 0; i < this.files.length; i++) + { + queue.push({ + file: this.files[i], + inputElem: this, + instanceConfig: $.extend({}, config) + }); + } + }); + + parseNextFile(); // begin parsing + return this; // maintains chainability + + + function parseNextFile() + { + if (queue.length === 0) + { + if (isFunction(options.complete)) + options.complete(); + return; + } + + var f = queue[0]; + + if (isFunction(options.before)) + { + var returned = options.before(f.file, f.inputElem); + + if (typeof returned === 'object') + { + if (returned.action === 'abort') + { + error('AbortError', f.file, f.inputElem, returned.reason); + return; // Aborts all queued files immediately + } + else if (returned.action === 'skip') + { + fileComplete(); // parse the next file in the queue, if any + return; + } + else if (typeof returned.config === 'object') + f.instanceConfig = $.extend(f.instanceConfig, returned.config); + } + else if (returned === 'skip') + { + fileComplete(); // parse the next file in the queue, if any + return; + } + } + + // Wrap up the user's complete callback, if any, so that ours also gets executed + var userCompleteFunc = f.instanceConfig.complete; + f.instanceConfig.complete = function(results) + { + if (isFunction(userCompleteFunc)) + userCompleteFunc(results, f.file, f.inputElem); + fileComplete(); + }; + + Papa.parse(f.file, f.instanceConfig); + } + + function error(name, file, elem, reason) + { + if (isFunction(options.error)) + options.error({name: name}, file, elem, reason); + } + + function fileComplete() + { + queue.splice(0, 1); + parseNextFile(); + } + }; + } + + + if (IS_PAPA_WORKER) + { + global.onmessage = workerThreadReceivedMessage; + } + + + + + function CsvToJson(_input, _config) + { + _config = _config || {}; + var dynamicTyping = _config.dynamicTyping || false; + if (isFunction(dynamicTyping)) { + _config.dynamicTypingFunction = dynamicTyping; + // Will be filled on first row call + dynamicTyping = {}; + } + _config.dynamicTyping = dynamicTyping; + + _config.transform = isFunction(_config.transform) ? _config.transform : false; + + if (_config.worker && Papa.WORKERS_SUPPORTED) + { + var w = newWorker(); + + w.userStep = _config.step; + w.userChunk = _config.chunk; + w.userComplete = _config.complete; + w.userError = _config.error; + + _config.step = isFunction(_config.step); + _config.chunk = isFunction(_config.chunk); + _config.complete = isFunction(_config.complete); + _config.error = isFunction(_config.error); + delete _config.worker; // prevent infinite loop + + w.postMessage({ + input: _input, + config: _config, + workerId: w.id + }); + + return; + } + + var streamer = null; + if (_input === Papa.NODE_STREAM_INPUT && typeof PAPA_BROWSER_CONTEXT === 'undefined') + { + // create a node Duplex stream for use + // with .pipe + streamer = new DuplexStreamStreamer(_config); + return streamer.getStream(); + } + else if (typeof _input === 'string') + { + _input = stripBom(_input); + if (_config.download) + streamer = new NetworkStreamer(_config); + else + streamer = new StringStreamer(_config); + } + else if (_input.readable === true && isFunction(_input.read) && isFunction(_input.on)) + { + streamer = new ReadableStreamStreamer(_config); + } + else if ((global.File && _input instanceof File) || _input instanceof Object) // ...Safari. (see issue #106) + streamer = new FileStreamer(_config); + + return streamer.stream(_input); + + // Strip character from UTF-8 BOM encoded files that cause issue parsing the file + function stripBom(string) { + if (string.charCodeAt(0) === 0xfeff) { + return string.slice(1); + } + return string; + } + } + + + + + + + function JsonToCsv(_input, _config) + { + // Default configuration + + /** whether to surround every datum with quotes */ + var _quotes = false; + + /** whether to write headers */ + var _writeHeader = true; + + /** delimiting character(s) */ + var _delimiter = ','; + + /** newline character(s) */ + var _newline = '\r\n'; + + /** quote character */ + var _quoteChar = '"'; + + /** escaped quote character, either "" or " */ + var _escapedQuote = _quoteChar + _quoteChar; + + /** whether to skip empty lines */ + var _skipEmptyLines = false; + + /** the columns (keys) we expect when we unparse objects */ + var _columns = null; + + /** whether to prevent outputting cells that can be parsed as formulae by spreadsheet software (Excel and LibreOffice) */ + var _escapeFormulae = false; + + unpackConfig(); + + var quoteCharRegex = new RegExp(escapeRegExp(_quoteChar), 'g'); + + if (typeof _input === 'string') + _input = JSON.parse(_input); + + if (Array.isArray(_input)) + { + if (!_input.length || Array.isArray(_input[0])) + return serialize(null, _input, _skipEmptyLines); + else if (typeof _input[0] === 'object') + return serialize(_columns || Object.keys(_input[0]), _input, _skipEmptyLines); + } + else if (typeof _input === 'object') + { + if (typeof _input.data === 'string') + _input.data = JSON.parse(_input.data); + + if (Array.isArray(_input.data)) + { + if (!_input.fields) + _input.fields = _input.meta && _input.meta.fields || _columns; + + if (!_input.fields) + _input.fields = Array.isArray(_input.data[0]) + ? _input.fields + : typeof _input.data[0] === 'object' + ? Object.keys(_input.data[0]) + : []; + + if (!(Array.isArray(_input.data[0])) && typeof _input.data[0] !== 'object') + _input.data = [_input.data]; // handles input like [1,2,3] or ['asdf'] + } + + return serialize(_input.fields || [], _input.data || [], _skipEmptyLines); + } + + // Default (any valid paths should return before this) + throw new Error('Unable to serialize unrecognized input'); + + + function unpackConfig() + { + if (typeof _config !== 'object') + return; + + if (typeof _config.delimiter === 'string' + && !Papa.BAD_DELIMITERS.filter(function(value) { return _config.delimiter.indexOf(value) !== -1; }).length) + { + _delimiter = _config.delimiter; + } + + if (typeof _config.quotes === 'boolean' + || typeof _config.quotes === 'function' + || Array.isArray(_config.quotes)) + _quotes = _config.quotes; + + if (typeof _config.skipEmptyLines === 'boolean' + || typeof _config.skipEmptyLines === 'string') + _skipEmptyLines = _config.skipEmptyLines; + + if (typeof _config.newline === 'string') + _newline = _config.newline; + + if (typeof _config.quoteChar === 'string') + _quoteChar = _config.quoteChar; + + if (typeof _config.header === 'boolean') + _writeHeader = _config.header; + + if (Array.isArray(_config.columns)) { + + if (_config.columns.length === 0) throw new Error('Option columns is empty'); + + _columns = _config.columns; + } + + if (_config.escapeChar !== undefined) { + _escapedQuote = _config.escapeChar + _quoteChar; + } + + if (typeof _config.escapeFormulae === 'boolean' || _config.escapeFormulae instanceof RegExp) { + _escapeFormulae = _config.escapeFormulae instanceof RegExp ? _config.escapeFormulae : /^[=+\-@\t\r].*$/; + } + } + + /** The double for loop that iterates the data and writes out a CSV string including header row */ + function serialize(fields, data, skipEmptyLines) + { + var csv = ''; + + if (typeof fields === 'string') + fields = JSON.parse(fields); + if (typeof data === 'string') + data = JSON.parse(data); + + var hasHeader = Array.isArray(fields) && fields.length > 0; + var dataKeyedByField = !(Array.isArray(data[0])); + + // If there a header row, write it first + if (hasHeader && _writeHeader) + { + for (var i = 0; i < fields.length; i++) + { + if (i > 0) + csv += _delimiter; + csv += safe(fields[i], i); + } + if (data.length > 0) + csv += _newline; + } + + // Then write out the data + for (var row = 0; row < data.length; row++) + { + var maxCol = hasHeader ? fields.length : data[row].length; + + var emptyLine = false; + var nullLine = hasHeader ? Object.keys(data[row]).length === 0 : data[row].length === 0; + if (skipEmptyLines && !hasHeader) + { + emptyLine = skipEmptyLines === 'greedy' ? data[row].join('').trim() === '' : data[row].length === 1 && data[row][0].length === 0; + } + if (skipEmptyLines === 'greedy' && hasHeader) { + var line = []; + for (var c = 0; c < maxCol; c++) { + var cx = dataKeyedByField ? fields[c] : c; + line.push(data[row][cx]); + } + emptyLine = line.join('').trim() === ''; + } + if (!emptyLine) + { + for (var col = 0; col < maxCol; col++) + { + if (col > 0 && !nullLine) + csv += _delimiter; + var colIdx = hasHeader && dataKeyedByField ? fields[col] : col; + csv += safe(data[row][colIdx], col); + } + if (row < data.length - 1 && (!skipEmptyLines || (maxCol > 0 && !nullLine))) + { + csv += _newline; + } + } + } + return csv; + } + + /** Encloses a value around quotes if needed (makes a value safe for CSV insertion) */ + function safe(str, col) + { + if (typeof str === 'undefined' || str === null) + return ''; + + if (str.constructor === Date) + return JSON.stringify(str).slice(1, 25); + + var needsQuotes = false; + + if (_escapeFormulae && typeof str === "string" && _escapeFormulae.test(str)) { + str = "'" + str; + needsQuotes = true; + } + + var escapedQuoteStr = str.toString().replace(quoteCharRegex, _escapedQuote); + + needsQuotes = needsQuotes + || _quotes === true + || (typeof _quotes === 'function' && _quotes(str, col)) + || (Array.isArray(_quotes) && _quotes[col]) + || hasAny(escapedQuoteStr, Papa.BAD_DELIMITERS) + || escapedQuoteStr.indexOf(_delimiter) > -1 + || escapedQuoteStr.charAt(0) === ' ' + || escapedQuoteStr.charAt(escapedQuoteStr.length - 1) === ' '; + + return needsQuotes ? _quoteChar + escapedQuoteStr + _quoteChar : escapedQuoteStr; + } + + function hasAny(str, substrings) + { + for (var i = 0; i < substrings.length; i++) + if (str.indexOf(substrings[i]) > -1) + return true; + return false; + } + } + + /** ChunkStreamer is the base prototype for various streamer implementations. */ + function ChunkStreamer(config) + { + this._handle = null; + this._finished = false; + this._completed = false; + this._halted = false; + this._input = null; + this._baseIndex = 0; + this._partialLine = ''; + this._rowCount = 0; + this._start = 0; + this._nextChunk = null; + this.isFirstChunk = true; + this._completeResults = { + data: [], + errors: [], + meta: {} + }; + replaceConfig.call(this, config); + + this.parseChunk = function(chunk, isFakeChunk) + { + // First chunk pre-processing + if (this.isFirstChunk && isFunction(this._config.beforeFirstChunk)) + { + var modifiedChunk = this._config.beforeFirstChunk(chunk); + if (modifiedChunk !== undefined) + chunk = modifiedChunk; + } + this.isFirstChunk = false; + this._halted = false; + + // Rejoin the line we likely just split in two by chunking the file + var aggregate = this._partialLine + chunk; + this._partialLine = ''; + + var results = this._handle.parse(aggregate, this._baseIndex, !this._finished); + + if (this._handle.paused() || this._handle.aborted()) { + this._halted = true; + return; + } + + var lastIndex = results.meta.cursor; + + if (!this._finished) + { + this._partialLine = aggregate.substring(lastIndex - this._baseIndex); + this._baseIndex = lastIndex; + } + + if (results && results.data) + this._rowCount += results.data.length; + + var finishedIncludingPreview = this._finished || (this._config.preview && this._rowCount >= this._config.preview); + + if (IS_PAPA_WORKER) + { + global.postMessage({ + results: results, + workerId: Papa.WORKER_ID, + finished: finishedIncludingPreview + }); + } + else if (isFunction(this._config.chunk) && !isFakeChunk) + { + this._config.chunk(results, this._handle); + if (this._handle.paused() || this._handle.aborted()) { + this._halted = true; + return; + } + results = undefined; + this._completeResults = undefined; + } + + if (!this._config.step && !this._config.chunk) { + this._completeResults.data = this._completeResults.data.concat(results.data); + this._completeResults.errors = this._completeResults.errors.concat(results.errors); + this._completeResults.meta = results.meta; + } + + if (!this._completed && finishedIncludingPreview && isFunction(this._config.complete) && (!results || !results.meta.aborted)) { + this._config.complete(this._completeResults, this._input); + this._completed = true; + } + + if (!finishedIncludingPreview && (!results || !results.meta.paused)) + this._nextChunk(); + + return results; + }; + + this._sendError = function(error) + { + if (isFunction(this._config.error)) + this._config.error(error); + else if (IS_PAPA_WORKER && this._config.error) + { + global.postMessage({ + workerId: Papa.WORKER_ID, + error: error, + finished: false + }); + } + }; + + function replaceConfig(config) + { + // Deep-copy the config so we can edit it + var configCopy = copy(config); + configCopy.chunkSize = parseInt(configCopy.chunkSize); // parseInt VERY important so we don't concatenate strings! + if (!config.step && !config.chunk) + configCopy.chunkSize = null; // disable Range header if not streaming; bad values break IIS - see issue #196 + this._handle = new ParserHandle(configCopy); + this._handle.streamer = this; + this._config = configCopy; // persist the copy to the caller + } + } + + + function NetworkStreamer(config) + { + config = config || {}; + if (!config.chunkSize) + config.chunkSize = Papa.RemoteChunkSize; + ChunkStreamer.call(this, config); + + var xhr; + + if (IS_WORKER) + { + this._nextChunk = function() + { + this._readChunk(); + this._chunkLoaded(); + }; + } + else + { + this._nextChunk = function() + { + this._readChunk(); + }; + } + + this.stream = function(url) + { + this._input = url; + this._nextChunk(); // Starts streaming + }; + + this._readChunk = function() + { + if (this._finished) + { + this._chunkLoaded(); + return; + } + + xhr = new XMLHttpRequest(); + + if (this._config.withCredentials) + { + xhr.withCredentials = this._config.withCredentials; + } + + if (!IS_WORKER) + { + xhr.onload = bindFunction(this._chunkLoaded, this); + xhr.onerror = bindFunction(this._chunkError, this); + } + + xhr.open(this._config.downloadRequestBody ? 'POST' : 'GET', this._input, !IS_WORKER); + // Headers can only be set when once the request state is OPENED + if (this._config.downloadRequestHeaders) + { + var headers = this._config.downloadRequestHeaders; + + for (var headerName in headers) + { + xhr.setRequestHeader(headerName, headers[headerName]); + } + } + + if (this._config.chunkSize) + { + var end = this._start + this._config.chunkSize - 1; // minus one because byte range is inclusive + xhr.setRequestHeader('Range', 'bytes=' + this._start + '-' + end); + } + + try { + xhr.send(this._config.downloadRequestBody); + } + catch (err) { + this._chunkError(err.message); + } + + if (IS_WORKER && xhr.status === 0) + this._chunkError(); + }; + + this._chunkLoaded = function() + { + if (xhr.readyState !== 4) + return; + + if (xhr.status < 200 || xhr.status >= 400) + { + this._chunkError(); + return; + } + + // Use chunckSize as it may be a diference on reponse lentgh due to characters with more than 1 byte + this._start += this._config.chunkSize ? this._config.chunkSize : xhr.responseText.length; + this._finished = !this._config.chunkSize || this._start >= getFileSize(xhr); + this.parseChunk(xhr.responseText); + }; + + this._chunkError = function(errorMessage) + { + var errorText = xhr.statusText || errorMessage; + this._sendError(new Error(errorText)); + }; + + function getFileSize(xhr) + { + var contentRange = xhr.getResponseHeader('Content-Range'); + if (contentRange === null) { // no content range, then finish! + return -1; + } + return parseInt(contentRange.substring(contentRange.lastIndexOf('/') + 1)); + } + } + NetworkStreamer.prototype = Object.create(ChunkStreamer.prototype); + NetworkStreamer.prototype.constructor = NetworkStreamer; + + + function FileStreamer(config) + { + config = config || {}; + if (!config.chunkSize) + config.chunkSize = Papa.LocalChunkSize; + ChunkStreamer.call(this, config); + + var reader, slice; + + // FileReader is better than FileReaderSync (even in worker) - see http://stackoverflow.com/q/24708649/1048862 + // But Firefox is a pill, too - see issue #76: https://github.com/mholt/PapaParse/issues/76 + var usingAsyncReader = typeof FileReader !== 'undefined'; // Safari doesn't consider it a function - see issue #105 + + this.stream = function(file) + { + this._input = file; + slice = file.slice || file.webkitSlice || file.mozSlice; + + if (usingAsyncReader) + { + reader = new FileReader(); // Preferred method of reading files, even in workers + reader.onload = bindFunction(this._chunkLoaded, this); + reader.onerror = bindFunction(this._chunkError, this); + } + else + reader = new FileReaderSync(); // Hack for running in a web worker in Firefox + + this._nextChunk(); // Starts streaming + }; + + this._nextChunk = function() + { + if (!this._finished && (!this._config.preview || this._rowCount < this._config.preview)) + this._readChunk(); + }; + + this._readChunk = function() + { + var input = this._input; + if (this._config.chunkSize) + { + var end = Math.min(this._start + this._config.chunkSize, this._input.size); + input = slice.call(input, this._start, end); + } + var txt = reader.readAsText(input, this._config.encoding); + if (!usingAsyncReader) + this._chunkLoaded({ target: { result: txt } }); // mimic the async signature + }; + + this._chunkLoaded = function(event) + { + // Very important to increment start each time before handling results + this._start += this._config.chunkSize; + this._finished = !this._config.chunkSize || this._start >= this._input.size; + this.parseChunk(event.target.result); + }; + + this._chunkError = function() + { + this._sendError(reader.error); + }; + + } + FileStreamer.prototype = Object.create(ChunkStreamer.prototype); + FileStreamer.prototype.constructor = FileStreamer; + + + function StringStreamer(config) + { + config = config || {}; + ChunkStreamer.call(this, config); + + var remaining; + this.stream = function(s) + { + remaining = s; + return this._nextChunk(); + }; + this._nextChunk = function() + { + if (this._finished) return; + var size = this._config.chunkSize; + var chunk; + if(size) { + chunk = remaining.substring(0, size); + remaining = remaining.substring(size); + } else { + chunk = remaining; + remaining = ''; + } + this._finished = !remaining; + return this.parseChunk(chunk); + }; + } + StringStreamer.prototype = Object.create(StringStreamer.prototype); + StringStreamer.prototype.constructor = StringStreamer; + + + function ReadableStreamStreamer(config) + { + config = config || {}; + + ChunkStreamer.call(this, config); + + var queue = []; + var parseOnData = true; + var streamHasEnded = false; + + this.pause = function() + { + ChunkStreamer.prototype.pause.apply(this, arguments); + this._input.pause(); + }; + + this.resume = function() + { + ChunkStreamer.prototype.resume.apply(this, arguments); + this._input.resume(); + }; + + this.stream = function(stream) + { + this._input = stream; + + this._input.on('data', this._streamData); + this._input.on('end', this._streamEnd); + this._input.on('error', this._streamError); + }; + + this._checkIsFinished = function() + { + if (streamHasEnded && queue.length === 1) { + this._finished = true; + } + }; + + this._nextChunk = function() + { + this._checkIsFinished(); + if (queue.length) + { + this.parseChunk(queue.shift()); + } + else + { + parseOnData = true; + } + }; + + this._streamData = bindFunction(function(chunk) + { + try + { + queue.push(typeof chunk === 'string' ? chunk : chunk.toString(this._config.encoding)); + + if (parseOnData) + { + parseOnData = false; + this._checkIsFinished(); + this.parseChunk(queue.shift()); + } + } + catch (error) + { + this._streamError(error); + } + }, this); + + this._streamError = bindFunction(function(error) + { + this._streamCleanUp(); + this._sendError(error); + }, this); + + this._streamEnd = bindFunction(function() + { + this._streamCleanUp(); + streamHasEnded = true; + this._streamData(''); + }, this); + + this._streamCleanUp = bindFunction(function() + { + this._input.removeListener('data', this._streamData); + this._input.removeListener('end', this._streamEnd); + this._input.removeListener('error', this._streamError); + }, this); + } + ReadableStreamStreamer.prototype = Object.create(ChunkStreamer.prototype); + ReadableStreamStreamer.prototype.constructor = ReadableStreamStreamer; + + + function DuplexStreamStreamer(_config) { + var Duplex = require('stream').Duplex; + var config = copy(_config); + var parseOnWrite = true; + var writeStreamHasFinished = false; + var parseCallbackQueue = []; + var stream = null; + + this._onCsvData = function(results) + { + var data = results.data; + if (!stream.push(data) && !this._handle.paused()) { + // the writeable consumer buffer has filled up + // so we need to pause until more items + // can be processed + this._handle.pause(); + } + }; + + this._onCsvComplete = function() + { + // node will finish the read stream when + // null is pushed + stream.push(null); + }; + + config.step = bindFunction(this._onCsvData, this); + config.complete = bindFunction(this._onCsvComplete, this); + ChunkStreamer.call(this, config); + + this._nextChunk = function() + { + if (writeStreamHasFinished && parseCallbackQueue.length === 1) { + this._finished = true; + } + if (parseCallbackQueue.length) { + parseCallbackQueue.shift()(); + } else { + parseOnWrite = true; + } + }; + + this._addToParseQueue = function(chunk, callback) + { + // add to queue so that we can indicate + // completion via callback + // node will automatically pause the incoming stream + // when too many items have been added without their + // callback being invoked + parseCallbackQueue.push(bindFunction(function() { + this.parseChunk(typeof chunk === 'string' ? chunk : chunk.toString(config.encoding)); + if (isFunction(callback)) { + return callback(); + } + }, this)); + if (parseOnWrite) { + parseOnWrite = false; + this._nextChunk(); + } + }; + + this._onRead = function() + { + if (this._handle.paused()) { + // the writeable consumer can handle more data + // so resume the chunk parsing + this._handle.resume(); + } + }; + + this._onWrite = function(chunk, encoding, callback) + { + this._addToParseQueue(chunk, callback); + }; + + this._onWriteComplete = function() + { + writeStreamHasFinished = true; + // have to write empty string + // so parser knows its done + this._addToParseQueue(''); + }; + + this.getStream = function() + { + return stream; + }; + stream = new Duplex({ + readableObjectMode: true, + decodeStrings: false, + read: bindFunction(this._onRead, this), + write: bindFunction(this._onWrite, this) + }); + stream.once('finish', bindFunction(this._onWriteComplete, this)); + } + if (typeof PAPA_BROWSER_CONTEXT === 'undefined') { + DuplexStreamStreamer.prototype = Object.create(ChunkStreamer.prototype); + DuplexStreamStreamer.prototype.constructor = DuplexStreamStreamer; + } + + + // Use one ParserHandle per entire CSV file or string + function ParserHandle(_config) + { + // One goal is to minimize the use of regular expressions... + var MAX_FLOAT = Math.pow(2, 53); + var MIN_FLOAT = -MAX_FLOAT; + var FLOAT = /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/; + var ISO_DATE = /^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/; + var self = this; + var _stepCounter = 0; // Number of times step was called (number of rows parsed) + var _rowCounter = 0; // Number of rows that have been parsed so far + var _input; // The input being parsed + var _parser; // The core parser being used + var _paused = false; // Whether we are paused or not + var _aborted = false; // Whether the parser has aborted or not + var _delimiterError; // Temporary state between delimiter detection and processing results + var _fields = []; // Fields are from the header row of the input, if there is one + var _results = { // The last results returned from the parser + data: [], + errors: [], + meta: {} + }; + + if (isFunction(_config.step)) + { + var userStep = _config.step; + _config.step = function(results) + { + _results = results; + + if (needsHeaderRow()) + processResults(); + else // only call user's step function after header row + { + processResults(); + + // It's possbile that this line was empty and there's no row here after all + if (_results.data.length === 0) + return; + + _stepCounter += results.data.length; + if (_config.preview && _stepCounter > _config.preview) + _parser.abort(); + else { + _results.data = _results.data[0]; + userStep(_results, self); + } + } + }; + } + + /** + * Parses input. Most users won't need, and shouldn't mess with, the baseIndex + * and ignoreLastRow parameters. They are used by streamers (wrapper functions) + * when an input comes in multiple chunks, like from a file. + */ + this.parse = function(input, baseIndex, ignoreLastRow) + { + var quoteChar = _config.quoteChar || '"'; + if (!_config.newline) + _config.newline = guessLineEndings(input, quoteChar); + + _delimiterError = false; + if (!_config.delimiter) + { + var delimGuess = guessDelimiter(input, _config.newline, _config.skipEmptyLines, _config.comments, _config.delimitersToGuess); + if (delimGuess.successful) + _config.delimiter = delimGuess.bestDelimiter; + else + { + _delimiterError = true; // add error after parsing (otherwise it would be overwritten) + _config.delimiter = Papa.DefaultDelimiter; + } + _results.meta.delimiter = _config.delimiter; + } + else if(isFunction(_config.delimiter)) + { + _config.delimiter = _config.delimiter(input); + _results.meta.delimiter = _config.delimiter; + } + + var parserConfig = copy(_config); + if (_config.preview && _config.header) + parserConfig.preview++; // to compensate for header row + + _input = input; + _parser = new Parser(parserConfig); + _results = _parser.parse(_input, baseIndex, ignoreLastRow); + processResults(); + return _paused ? { meta: { paused: true } } : (_results || { meta: { paused: false } }); + }; + + this.paused = function() + { + return _paused; + }; + + this.pause = function() + { + _paused = true; + _parser.abort(); + + // If it is streaming via "chunking", the reader will start appending correctly already so no need to substring, + // otherwise we can get duplicate content within a row + _input = isFunction(_config.chunk) ? "" : _input.substring(_parser.getCharIndex()); + }; + + this.resume = function() + { + if(self.streamer._halted) { + _paused = false; + self.streamer.parseChunk(_input, true); + } else { + // Bugfix: #636 In case the processing hasn't halted yet + // wait for it to halt in order to resume + setTimeout(self.resume, 3); + } + }; + + this.aborted = function() + { + return _aborted; + }; + + this.abort = function() + { + _aborted = true; + _parser.abort(); + _results.meta.aborted = true; + if (isFunction(_config.complete)) + _config.complete(_results); + _input = ''; + }; + + function testEmptyLine(s) { + return _config.skipEmptyLines === 'greedy' ? s.join('').trim() === '' : s.length === 1 && s[0].length === 0; + } + + function testFloat(s) { + if (FLOAT.test(s)) { + var floatValue = parseFloat(s); + if (floatValue > MIN_FLOAT && floatValue < MAX_FLOAT) { + return true; + } + } + return false; + } + + function processResults() + { + if (_results && _delimiterError) + { + addError('Delimiter', 'UndetectableDelimiter', 'Unable to auto-detect delimiting character; defaulted to \'' + Papa.DefaultDelimiter + '\''); + _delimiterError = false; + } + + if (_config.skipEmptyLines) + { + _results.data = _results.data.filter(function(d) { + return !testEmptyLine(d); + }); + } + + if (needsHeaderRow()) + fillHeaderFields(); + + return applyHeaderAndDynamicTypingAndTransformation(); + } + + function needsHeaderRow() + { + return _config.header && _fields.length === 0; + } + + function fillHeaderFields() + { + if (!_results) + return; + + function addHeader(header, i) + { + if (isFunction(_config.transformHeader)) + header = _config.transformHeader(header, i); + + _fields.push(header); + } + + if (Array.isArray(_results.data[0])) + { + for (var i = 0; needsHeaderRow() && i < _results.data.length; i++) + _results.data[i].forEach(addHeader); + + _results.data.splice(0, 1); + } + // if _results.data[0] is not an array, we are in a step where _results.data is the row. + else + _results.data.forEach(addHeader); + } + + function shouldApplyDynamicTyping(field) { + // Cache function values to avoid calling it for each row + if (_config.dynamicTypingFunction && _config.dynamicTyping[field] === undefined) { + _config.dynamicTyping[field] = _config.dynamicTypingFunction(field); + } + return (_config.dynamicTyping[field] || _config.dynamicTyping) === true; + } + + function parseDynamic(field, value) + { + if (shouldApplyDynamicTyping(field)) + { + if (value === 'true' || value === 'TRUE') + return true; + else if (value === 'false' || value === 'FALSE') + return false; + else if (testFloat(value)) + return parseFloat(value); + else if (ISO_DATE.test(value)) + return new Date(value); + else + return (value === '' ? null : value); + } + return value; + } + + function applyHeaderAndDynamicTypingAndTransformation() + { + if (!_results || (!_config.header && !_config.dynamicTyping && !_config.transform)) + return _results; + + function processRow(rowSource, i) + { + var row = _config.header ? {} : []; + + var j; + for (j = 0; j < rowSource.length; j++) + { + var field = j; + var value = rowSource[j]; + + if (_config.header) + field = j >= _fields.length ? '__parsed_extra' : _fields[j]; + + if (_config.transform) + value = _config.transform(value,field); + + value = parseDynamic(field, value); + + if (field === '__parsed_extra') + { + row[field] = row[field] || []; + row[field].push(value); + } + else + row[field] = value; + } + + + if (_config.header) + { + if (j > _fields.length) + addError('FieldMismatch', 'TooManyFields', 'Too many fields: expected ' + _fields.length + ' fields but parsed ' + j, _rowCounter + i); + else if (j < _fields.length) + addError('FieldMismatch', 'TooFewFields', 'Too few fields: expected ' + _fields.length + ' fields but parsed ' + j, _rowCounter + i); + } + + return row; + } + + var incrementBy = 1; + if (!_results.data.length || Array.isArray(_results.data[0])) + { + _results.data = _results.data.map(processRow); + incrementBy = _results.data.length; + } + else + _results.data = processRow(_results.data, 0); + + + if (_config.header && _results.meta) + _results.meta.fields = _fields; + + _rowCounter += incrementBy; + return _results; + } + + function guessDelimiter(input, newline, skipEmptyLines, comments, delimitersToGuess) { + var bestDelim, bestDelta, fieldCountPrevRow, maxFieldCount; + + delimitersToGuess = delimitersToGuess || [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]; + + for (var i = 0; i < delimitersToGuess.length; i++) { + var delim = delimitersToGuess[i]; + var delta = 0, avgFieldCount = 0, emptyLinesCount = 0; + fieldCountPrevRow = undefined; + + var preview = new Parser({ + comments: comments, + delimiter: delim, + newline: newline, + preview: 10 + }).parse(input); + + for (var j = 0; j < preview.data.length; j++) { + if (skipEmptyLines && testEmptyLine(preview.data[j])) { + emptyLinesCount++; + continue; + } + var fieldCount = preview.data[j].length; + avgFieldCount += fieldCount; + + if (typeof fieldCountPrevRow === 'undefined') { + fieldCountPrevRow = fieldCount; + continue; + } + else if (fieldCount > 0) { + delta += Math.abs(fieldCount - fieldCountPrevRow); + fieldCountPrevRow = fieldCount; + } + } + + if (preview.data.length > 0) + avgFieldCount /= (preview.data.length - emptyLinesCount); + + if ((typeof bestDelta === 'undefined' || delta <= bestDelta) + && (typeof maxFieldCount === 'undefined' || avgFieldCount > maxFieldCount) && avgFieldCount > 1.99) { + bestDelta = delta; + bestDelim = delim; + maxFieldCount = avgFieldCount; + } + } + + _config.delimiter = bestDelim; + + return { + successful: !!bestDelim, + bestDelimiter: bestDelim + }; + } + + function guessLineEndings(input, quoteChar) + { + input = input.substring(0, 1024 * 1024); // max length 1 MB + // Replace all the text inside quotes + var re = new RegExp(escapeRegExp(quoteChar) + '([^]*?)' + escapeRegExp(quoteChar), 'gm'); + input = input.replace(re, ''); + + var r = input.split('\r'); + + var n = input.split('\n'); + + var nAppearsFirst = (n.length > 1 && n[0].length < r[0].length); + + if (r.length === 1 || nAppearsFirst) + return '\n'; + + var numWithN = 0; + for (var i = 0; i < r.length; i++) + { + if (r[i][0] === '\n') + numWithN++; + } + + return numWithN >= r.length / 2 ? '\r\n' : '\r'; + } + + function addError(type, code, msg, row) + { + var error = { + type: type, + code: code, + message: msg + }; + if(row !== undefined) { + error.row = row; + } + _results.errors.push(error); + } + } + + /** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions */ + function escapeRegExp(string) + { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + + /** The core parser implements speedy and correct CSV parsing */ + function Parser(config) + { + // Unpack the config object + config = config || {}; + var delim = config.delimiter; + var newline = config.newline; + var comments = config.comments; + var step = config.step; + var preview = config.preview; + var fastMode = config.fastMode; + var quoteChar; + if (config.quoteChar === undefined || config.quoteChar === null) { + quoteChar = '"'; + } else { + quoteChar = config.quoteChar; + } + var escapeChar = quoteChar; + if (config.escapeChar !== undefined) { + escapeChar = config.escapeChar; + } + + // Delimiter must be valid + if (typeof delim !== 'string' + || Papa.BAD_DELIMITERS.indexOf(delim) > -1) + delim = ','; + + // Comment character must be valid + if (comments === delim) + throw new Error('Comment character same as delimiter'); + else if (comments === true) + comments = '#'; + else if (typeof comments !== 'string' + || Papa.BAD_DELIMITERS.indexOf(comments) > -1) + comments = false; + + // Newline must be valid: \r, \n, or \r\n + if (newline !== '\n' && newline !== '\r' && newline !== '\r\n') + newline = '\n'; + + // We're gonna need these at the Parser scope + var cursor = 0; + var aborted = false; + + this.parse = function(input, baseIndex, ignoreLastRow) + { + // For some reason, in Chrome, this speeds things up (!?) + if (typeof input !== 'string') + throw new Error('Input must be a string'); + + // We don't need to compute some of these every time parse() is called, + // but having them in a more local scope seems to perform better + var inputLen = input.length, + delimLen = delim.length, + newlineLen = newline.length, + commentsLen = comments.length; + var stepIsFunction = isFunction(step); + + // Establish starting state + cursor = 0; + var data = [], errors = [], row = [], lastCursor = 0; + + if (!input) + return returnable(); + + // Rename headers if there are duplicates + if (config.header && !baseIndex) + { + var firstLine = input.split(newline)[0]; + var headers = firstLine.split(delim); + var separator = '_'; + var headerMap = []; + var headerCount = {}; + var duplicateHeaders = false; + + for (var j in headers) { + var header = headers[j]; + if (isFunction(config.transformHeader)) + header = config.transformHeader(header, j); + var headerName = header; + + var count = headerCount[header] || 0; + if (count > 0) { + duplicateHeaders = true; + headerName = header + separator + count; + } + headerCount[header] = count + 1; + // In case it already exists, we add more separtors + while (headerMap.includes(headerName)) { + headerName = headerName + separator + count; + } + headerMap.push(headerName); + } + if (duplicateHeaders) { + var editedInput = input.split(newline); + editedInput[0] = headerMap.join(delim); + input = editedInput.join(newline); + } + } + if (fastMode || (fastMode !== false && input.indexOf(quoteChar) === -1)) + { + var rows = input.split(newline); + for (var i = 0; i < rows.length; i++) + { + row = rows[i]; + cursor += row.length; + if (i !== rows.length - 1) + cursor += newline.length; + else if (ignoreLastRow) + return returnable(); + if (comments && row.substring(0, commentsLen) === comments) + continue; + if (stepIsFunction) + { + data = []; + pushRow(row.split(delim)); + doStep(); + if (aborted) + return returnable(); + } + else + pushRow(row.split(delim)); + if (preview && i >= preview) + { + data = data.slice(0, preview); + return returnable(true); + } + } + return returnable(); + } + + var nextDelim = input.indexOf(delim, cursor); + var nextNewline = input.indexOf(newline, cursor); + var quoteCharRegex = new RegExp(escapeRegExp(escapeChar) + escapeRegExp(quoteChar), 'g'); + var quoteSearch = input.indexOf(quoteChar, cursor); + + // Parser loop + for (;;) + { + // Field has opening quote + if (input[cursor] === quoteChar) + { + // Start our search for the closing quote where the cursor is + quoteSearch = cursor; + + // Skip the opening quote + cursor++; + + for (;;) + { + // Find closing quote + quoteSearch = input.indexOf(quoteChar, quoteSearch + 1); + + //No other quotes are found - no other delimiters + if (quoteSearch === -1) + { + if (!ignoreLastRow) { + // No closing quote... what a pity + errors.push({ + type: 'Quotes', + code: 'MissingQuotes', + message: 'Quoted field unterminated', + row: data.length, // row has yet to be inserted + index: cursor + }); + } + return finish(); + } + + // Closing quote at EOF + if (quoteSearch === inputLen - 1) + { + var value = input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar); + return finish(value); + } + + // If this quote is escaped, it's part of the data; skip it + // If the quote character is the escape character, then check if the next character is the escape character + if (quoteChar === escapeChar && input[quoteSearch + 1] === escapeChar) + { + quoteSearch++; + continue; + } + + // If the quote character is not the escape character, then check if the previous character was the escape character + if (quoteChar !== escapeChar && quoteSearch !== 0 && input[quoteSearch - 1] === escapeChar) + { + continue; + } + + if(nextDelim !== -1 && nextDelim < (quoteSearch + 1)) { + nextDelim = input.indexOf(delim, (quoteSearch + 1)); + } + if(nextNewline !== -1 && nextNewline < (quoteSearch + 1)) { + nextNewline = input.indexOf(newline, (quoteSearch + 1)); + } + // Check up to nextDelim or nextNewline, whichever is closest + var checkUpTo = nextNewline === -1 ? nextDelim : Math.min(nextDelim, nextNewline); + var spacesBetweenQuoteAndDelimiter = extraSpaces(checkUpTo); + + // Closing quote followed by delimiter or 'unnecessary spaces + delimiter' + if (input.substr(quoteSearch + 1 + spacesBetweenQuoteAndDelimiter, delimLen) === delim) + { + row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar)); + cursor = quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen; + + // If char after following delimiter is not quoteChar, we find next quote char position + if (input[quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen] !== quoteChar) + { + quoteSearch = input.indexOf(quoteChar, cursor); + } + nextDelim = input.indexOf(delim, cursor); + nextNewline = input.indexOf(newline, cursor); + break; + } + + var spacesBetweenQuoteAndNewLine = extraSpaces(nextNewline); + + // Closing quote followed by newline or 'unnecessary spaces + newLine' + if (input.substring(quoteSearch + 1 + spacesBetweenQuoteAndNewLine, quoteSearch + 1 + spacesBetweenQuoteAndNewLine + newlineLen) === newline) + { + row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar)); + saveRow(quoteSearch + 1 + spacesBetweenQuoteAndNewLine + newlineLen); + nextDelim = input.indexOf(delim, cursor); // because we may have skipped the nextDelim in the quoted field + quoteSearch = input.indexOf(quoteChar, cursor); // we search for first quote in next line + + if (stepIsFunction) + { + doStep(); + if (aborted) + return returnable(); + } + + if (preview && data.length >= preview) + return returnable(true); + + break; + } + + + // Checks for valid closing quotes are complete (escaped quotes or quote followed by EOF/delimiter/newline) -- assume these quotes are part of an invalid text string + errors.push({ + type: 'Quotes', + code: 'InvalidQuotes', + message: 'Trailing quote on quoted field is malformed', + row: data.length, // row has yet to be inserted + index: cursor + }); + + quoteSearch++; + continue; + + } + + continue; + } + + // Comment found at start of new line + if (comments && row.length === 0 && input.substring(cursor, cursor + commentsLen) === comments) + { + if (nextNewline === -1) // Comment ends at EOF + return returnable(); + cursor = nextNewline + newlineLen; + nextNewline = input.indexOf(newline, cursor); + nextDelim = input.indexOf(delim, cursor); + continue; + } + + // Next delimiter comes before next newline, so we've reached end of field + if (nextDelim !== -1 && (nextDelim < nextNewline || nextNewline === -1)) + { + row.push(input.substring(cursor, nextDelim)); + cursor = nextDelim + delimLen; + // we look for next delimiter char + nextDelim = input.indexOf(delim, cursor); + continue; + } + + // End of row + if (nextNewline !== -1) + { + row.push(input.substring(cursor, nextNewline)); + saveRow(nextNewline + newlineLen); + + if (stepIsFunction) + { + doStep(); + if (aborted) + return returnable(); + } + + if (preview && data.length >= preview) + return returnable(true); + + continue; + } + + break; + } + + + return finish(); + + + function pushRow(row) + { + data.push(row); + lastCursor = cursor; + } + + /** + * checks if there are extra spaces after closing quote and given index without any text + * if Yes, returns the number of spaces + */ + function extraSpaces(index) { + var spaceLength = 0; + if (index !== -1) { + var textBetweenClosingQuoteAndIndex = input.substring(quoteSearch + 1, index); + if (textBetweenClosingQuoteAndIndex && textBetweenClosingQuoteAndIndex.trim() === '') { + spaceLength = textBetweenClosingQuoteAndIndex.length; + } + } + return spaceLength; + } + + /** + * Appends the remaining input from cursor to the end into + * row, saves the row, calls step, and returns the results. + */ + function finish(value) + { + if (ignoreLastRow) + return returnable(); + if (typeof value === 'undefined') + value = input.substring(cursor); + row.push(value); + cursor = inputLen; // important in case parsing is paused + pushRow(row); + if (stepIsFunction) + doStep(); + return returnable(); + } + + /** + * Appends the current row to the results. It sets the cursor + * to newCursor and finds the nextNewline. The caller should + * take care to execute user's step function and check for + * preview and end parsing if necessary. + */ + function saveRow(newCursor) + { + cursor = newCursor; + pushRow(row); + row = []; + nextNewline = input.indexOf(newline, cursor); + } + + /** Returns an object with the results, errors, and meta. */ + function returnable(stopped) + { + return { + data: data, + errors: errors, + meta: { + delimiter: delim, + linebreak: newline, + aborted: aborted, + truncated: !!stopped, + cursor: lastCursor + (baseIndex || 0) + } + }; + } + + /** Executes the user's step function and resets data & errors. */ + function doStep() + { + step(returnable()); + data = []; + errors = []; + } + }; + + /** Sets the abort flag */ + this.abort = function() + { + aborted = true; + }; + + /** Gets the cursor position */ + this.getCharIndex = function() + { + return cursor; + }; + } + + + function newWorker() + { + if (!Papa.WORKERS_SUPPORTED) + return false; + + var workerUrl = getWorkerBlob(); + var w = new global.Worker(workerUrl); + w.onmessage = mainThreadReceivedMessage; + w.id = workerIdCounter++; + workers[w.id] = w; + return w; + } + + /** Callback when main thread receives a message */ + function mainThreadReceivedMessage(e) + { + var msg = e.data; + var worker = workers[msg.workerId]; + var aborted = false; + + if (msg.error) + worker.userError(msg.error, msg.file); + else if (msg.results && msg.results.data) + { + var abort = function() { + aborted = true; + completeWorker(msg.workerId, { data: [], errors: [], meta: { aborted: true } }); + }; + + var handle = { + abort: abort, + pause: notImplemented, + resume: notImplemented + }; + + if (isFunction(worker.userStep)) + { + for (var i = 0; i < msg.results.data.length; i++) + { + worker.userStep({ + data: msg.results.data[i], + errors: msg.results.errors, + meta: msg.results.meta + }, handle); + if (aborted) + break; + } + delete msg.results; // free memory ASAP + } + else if (isFunction(worker.userChunk)) + { + worker.userChunk(msg.results, handle, msg.file); + delete msg.results; + } + } + + if (msg.finished && !aborted) + completeWorker(msg.workerId, msg.results); + } + + function completeWorker(workerId, results) { + var worker = workers[workerId]; + if (isFunction(worker.userComplete)) + worker.userComplete(results); + worker.terminate(); + delete workers[workerId]; + } + + function notImplemented() { + throw new Error('Not implemented.'); + } + + /** Callback when worker thread receives a message */ + function workerThreadReceivedMessage(e) + { + var msg = e.data; + + if (typeof Papa.WORKER_ID === 'undefined' && msg) + Papa.WORKER_ID = msg.workerId; + + if (typeof msg.input === 'string') + { + global.postMessage({ + workerId: Papa.WORKER_ID, + results: Papa.parse(msg.input, msg.config), + finished: true + }); + } + else if ((global.File && msg.input instanceof File) || msg.input instanceof Object) // thank you, Safari (see issue #106) + { + var results = Papa.parse(msg.input, msg.config); + if (results) + global.postMessage({ + workerId: Papa.WORKER_ID, + results: results, + finished: true + }); + } + } + + /** Makes a deep copy of an array or object (mostly) */ + function copy(obj) + { + if (typeof obj !== 'object' || obj === null) + return obj; + var cpy = Array.isArray(obj) ? [] : {}; + for (var key in obj) + cpy[key] = copy(obj[key]); + return cpy; + } + + function bindFunction(f, self) + { + return function() { f.apply(self, arguments); }; + } + + function isFunction(func) + { + return typeof func === 'function'; + } + + return Papa; +})); diff --git a/node_modules/papaparse/papaparse.min.js b/node_modules/papaparse/papaparse.min.js new file mode 100644 index 0000000..eeaf983 --- /dev/null +++ b/node_modules/papaparse/papaparse.min.js @@ -0,0 +1,7 @@ +/* @license +Papa Parse +v5.4.1 +https://github.com/mholt/PapaParse +License: MIT +*/ +!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=f.IS_PAPA_WORKER||!1,a={},u=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;J(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!J(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ","(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=u++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=J(t.step),t.chunk=J(t.chunk),t.complete=J(t.complete),t.error=J(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?(e=function(e){if(65279===e.charCodeAt(0))return e.slice(1);return e}(e),n=t.download?new l(t):new p(t)):!0===e.readable&&J(e.read)&&J(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,r=!1,i=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");i=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);("boolean"==typeof t.escapeFormulae||t.escapeFormulae instanceof RegExp)&&(o=t.escapeFormulae instanceof RegExp?t.escapeFormulae:/^[=+\-@\t\r].*$/)}();var u=new RegExp(Q(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return h(null,e,r);if("object"==typeof e[0])return h(i||Object.keys(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||i),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),h(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(J(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!J(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){J(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),h.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)),i.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===i.status&&this._chunkError()}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:i.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),h.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;h.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=r.substring(0,t),r=r.substring(t)):(e=r,r=""),this._finished=!r,this.parseChunk(e)}}}function g(e){h.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){h.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){h.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(m){var a,o,u,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,h=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(J(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;r+=e.data.length,m.preview&&r>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){return c&&u&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),u=!1),m.skipEmptyLines&&(c.data=c.data.filter(function(e){return!y(e)})),_()&&function(){if(!c)return;function e(e,t){J(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t=l.length?"__parsed_extra":l[r]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return m.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),u=!1,m.delimiter)J(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,u;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var h=0;h=N)return L(!0)}else for(S=W,W++;;){if(-1===(S=i.indexOf(z,S+1)))return r||h.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:u.length,index:W}),T();if(S===n-1)return T(i.substring(W,S).replace(C,z));if(z!==K||i[S+1]!==K){if(z===K||0===S||i[S-1]!==K){-1!==w&&w=N)return L(!0);break}h.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:u.length,index:W}),S++}}else S++}return T();function I(e){u.push(e),d=W}function A(e){var t=0;if(-1!==e){var r=i.substring(S+1,e);r&&""===r.trim()&&(t=r.length)}return t}function T(e){return r||(void 0===e&&(e=i.substring(W)),f.push(e),W=n,I(f),o&&F()),L()}function D(e){W=e,I(f),f=[],R=i.indexOf(P,W)}function L(e){return{data:u,errors:h,meta:{delimiter:M,linebreak:P,aborted:H,truncated:!!e,cursor:d+(t||0)}}}function F(){q(L()),u=[],h=[]}},this.abort=function(){H=!0},this.getCharIndex=function(){return W}}function _(e){var t=e.data,r=a[t.workerId],i=!1;if(t.error)r.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){i=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(J(r.userStep)){for(var s=0;s + + + Papa Parse Player + + + + + + + +

Papa Parse Player

+ +
+ +
+ + + + + + + + + + + + + + Line Endings: + + + + + + + + + + + + + + + +
+ +
+ + + +
+ or +
+ + + +

+ + +   + + +

+ + Open the Console in your browser's inspector tools to see results. +
+ +
+ + diff --git a/node_modules/papaparse/player/player.js b/node_modules/papaparse/player/player.js new file mode 100644 index 0000000..8150de6 --- /dev/null +++ b/node_modules/papaparse/player/player.js @@ -0,0 +1,180 @@ +var stepped = 0, chunks = 0, rows = 0; +var start, end; +var parser; +var pauseChecked = false; +var printStepChecked = false; + +$(function() +{ + $('#submit-parse').click(function() + { + stepped = 0; + chunks = 0; + rows = 0; + + var txt = $('#input').val(); + var localChunkSize = $('#localChunkSize').val(); + var remoteChunkSize = $('#remoteChunkSize').val(); + var files = $('#files')[0].files; + var config = buildConfig(); + + // NOTE: Chunk size does not get reset if changed and then set back to empty/default value + if (localChunkSize) + Papa.LocalChunkSize = localChunkSize; + if (remoteChunkSize) + Papa.RemoteChunkSize = remoteChunkSize; + + pauseChecked = $('#step-pause').prop('checked'); + printStepChecked = $('#print-steps').prop('checked'); + + + if (files.length > 0) + { + if (!$('#stream').prop('checked') && !$('#chunk').prop('checked')) + { + for (var i = 0; i < files.length; i++) + { + if (files[i].size > 1024 * 1024 * 10) + { + alert("A file you've selected is larger than 10 MB; please choose to stream or chunk the input to prevent the browser from crashing."); + return; + } + } + } + + start = performance.now(); + + $('#files').parse({ + config: config, + before: function(file, inputElem) + { + console.log("Parsing file:", file); + }, + complete: function() + { + console.log("Done with all files."); + } + }); + } + else + { + start = performance.now(); + var results = Papa.parse(txt, config); + console.log("Synchronous parse results:", results); + } + }); + + $('#submit-unparse').click(function() + { + var input = $('#input').val(); + var delim = $('#delimiter').val(); + var header = $('#header').prop('checked'); + + var results = Papa.unparse(input, { + delimiter: delim, + header: header, + }); + + console.log("Unparse complete!"); + console.log("--------------------------------------"); + console.log(results); + console.log("--------------------------------------"); + }); + + $('#insert-tab').click(function() + { + $('#delimiter').val('\t'); + }); +}); + + + +function buildConfig() +{ + return { + delimiter: $('#delimiter').val(), + newline: getLineEnding(), + header: $('#header').prop('checked'), + dynamicTyping: $('#dynamicTyping').prop('checked'), + preview: parseInt($('#preview').val() || 0), + step: $('#stream').prop('checked') ? stepFn : undefined, + encoding: $('#encoding').val(), + worker: $('#worker').prop('checked'), + comments: $('#comments').val(), + complete: completeFn, + error: errorFn, + download: $('#download').prop('checked'), + fastMode: $('#fastmode').prop('checked'), + skipEmptyLines: $('#skipEmptyLines').prop('checked'), + chunk: $('#chunk').prop('checked') ? chunkFn : undefined, + beforeFirstChunk: undefined, + }; + + function getLineEnding() + { + if ($('#newline-n').is(':checked')) + return "\n"; + else if ($('#newline-r').is(':checked')) + return "\r"; + else if ($('#newline-rn').is(':checked')) + return "\r\n"; + else + return ""; + } +} + +function stepFn(results, parserHandle) +{ + stepped++; + rows += results.data.length; + + parser = parserHandle; + + if (pauseChecked) + { + console.log(results, results.data[0]); + parserHandle.pause(); + return; + } + + if (printStepChecked) + console.log(results, results.data[0]); +} + +function chunkFn(results, streamer, file) +{ + if (!results) + return; + chunks++; + rows += results.data.length; + + parser = streamer; + + if (printStepChecked) + console.log("Chunk data:", results.data.length, results); + + if (pauseChecked) + { + console.log("Pausing; " + results.data.length + " rows in chunk; file:", file); + streamer.pause(); + return; + } +} + +function errorFn(error, file) +{ + console.log("ERROR:", error, file); +} + +function completeFn() +{ + end = performance.now(); + if (!$('#stream').prop('checked') + && !$('#chunk').prop('checked') + && arguments[0] + && arguments[0].data) + rows = arguments[0].data.length; + + console.log("Finished input (async). Time:", end-start, arguments); + console.log("Rows:", rows, "Stepped:", stepped, "Chunks:", chunks); +} diff --git a/node_modules/papaparse/tests/.eslintrc.js b/node_modules/papaparse/tests/.eslintrc.js new file mode 100644 index 0000000..f75898e --- /dev/null +++ b/node_modules/papaparse/tests/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + "extends": ["../.eslintrc.js"], + "parserOptions": { + "ecmaVersion": 8 + }, + "env": { + "mocha": true + }, + "rules": { + + } +}; diff --git a/node_modules/papaparse/tests/long-sample.csv b/node_modules/papaparse/tests/long-sample.csv new file mode 100644 index 0000000..59f0994 --- /dev/null +++ b/node_modules/papaparse/tests/long-sample.csv @@ -0,0 +1,8 @@ +Grant,Dyer,Donec.elementum@orciluctuset.example,2013-11-23T02:30:31-08:00,2014-05-31T01:06:56-07:00,Magna Ut Associates,ljenkins +Cherokee,Shields,Nulla.Semper.Tellus@duinec.example,2014-11-22T16:43:51-08:00,2013-09-26T11:47:15-07:00,Pede Corporation,Donec.elementum@orciluctuset.example +Catherine,Parrish,lorem@feugiatnon.example,2015-02-11T12:01:10-08:00,2015-02-26T00:29:40-08:00,Phasellus Fermentum Convallis PC,Donec.elementum@orciluctuset.example +Destiny,Shannon,libero@Aenean.example,2015-07-14T09:38:11-07:00,2014-01-11T14:53:04-08:00,Pretium Et Inc.,Donec.elementum@orciluctuset.example +Callum,Underwood,Phasellus@Quisquetincidunt.example,2013-09-13T18:49:35-07:00,2014-12-04T23:04:19-08:00,Sed Turpis Nec LLP,ljenkins +Elliott,Wright,cursus@nibh.example,2015-04-20T14:35:19-07:00,2015-03-05T12:56:46-08:00,Dolor Associate,Phasellus@Quisquetincidunt.example +Galvin,Foley,nisi.Aenean.eget@atauctorullamcorper.example,2014-03-20T23:20:15-07:00,2014-06-11T15:00:23-07:00,Adipiscing Industrie,Phasellus@Quisquetincidunt.example +Talon,Salinas,posuere.vulputate.lacus@Donecsollicitudin.example,2015-01-31T09:19:02-08:00,2014-12-17T04:59:18-08:00,Aliquam Iaculis Incorporate,Phasellus@Quisquetincidunt.example \ No newline at end of file diff --git a/node_modules/papaparse/tests/node-tests.js b/node_modules/papaparse/tests/node-tests.js new file mode 100644 index 0000000..6d012ec --- /dev/null +++ b/node_modules/papaparse/tests/node-tests.js @@ -0,0 +1,301 @@ +"use strict"; + +var Papa = require("../papaparse.js"); + +var fs = require('fs'); +var assert = require('assert'); +var longSampleRawCsv = fs.readFileSync(__dirname + '/long-sample.csv', 'utf8'); +var utf8BomSampleRawCsv = fs.readFileSync(__dirname + '/utf-8-bom-sample.csv', 'utf8'); + +function assertLongSampleParsedCorrectly(parsedCsv) { + assert.equal(8, parsedCsv.data.length); + assert.deepEqual(parsedCsv.data[0], [ + 'Grant', + 'Dyer', + 'Donec.elementum@orciluctuset.example', + '2013-11-23T02:30:31-08:00', + '2014-05-31T01:06:56-07:00', + 'Magna Ut Associates', + 'ljenkins' + ]); + assert.deepEqual(parsedCsv.data[7], [ + 'Talon', + 'Salinas', + 'posuere.vulputate.lacus@Donecsollicitudin.example', + '2015-01-31T09:19:02-08:00', + '2014-12-17T04:59:18-08:00', + 'Aliquam Iaculis Incorporate', + 'Phasellus@Quisquetincidunt.example' + ]); + assert.deepEqual(parsedCsv.meta, { + "delimiter": ",", + "linebreak": "\n", + "aborted": false, + "truncated": false, + "cursor": 1209 + }); + assert.equal(parsedCsv.errors.length, 0); +} + +describe('PapaParse', function() { + it('synchronously parsed CSV should be correctly parsed', function() { + assertLongSampleParsedCorrectly(Papa.parse(longSampleRawCsv)); + }); + + it('Pause and resume works (Regression Test for Bug #636)', function(done) { + this.timeout(30000); + var mod200Rows = [ + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Lorem ipsum dolor sit","42"] + ]; + var stepped = 0; + var dataRows = []; + Papa.parse(fs.createReadStream(__dirname + '/verylong-sample.csv'), { + step: function(results, parser) { + stepped++; + if (results) + { + parser.pause(); + parser.resume(); + if (results.data && stepped % 200 === 0) { + dataRows.push(results.data); + } + } + }, + complete: function() { + assert.strictEqual(2001, stepped); + assert.deepEqual(mod200Rows, dataRows); + done(); + } + }); + }); + + it('asynchronously parsed CSV should be correctly parsed', function(done) { + Papa.parse(longSampleRawCsv, { + complete: function(parsedCsv) { + assertLongSampleParsedCorrectly(parsedCsv); + done(); + }, + }); + }); + + it('asynchronously parsed streaming CSV should be correctly parsed', function(done) { + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + complete: function(parsedCsv) { + assertLongSampleParsedCorrectly(parsedCsv); + done(); + }, + }); + }); + + it('reports the correct row number on FieldMismatch errors', function(done) { + Papa.parse(fs.createReadStream(__dirname + '/verylong-sample.csv'), { + header: true, + fastMode: true, + complete: function(parsedCsv) { + assert.deepEqual(parsedCsv.errors, [ + { + "type": "FieldMismatch", + "code": "TooFewFields", + "message": "Too few fields: expected 3 fields but parsed 2", + "row": 498 + }, + { + "type": "FieldMismatch", + "code": "TooFewFields", + "message": "Too few fields: expected 3 fields but parsed 2", + "row": 998 + }, + { + "type": "FieldMismatch", + "code": "TooFewFields", + "message": "Too few fields: expected 3 fields but parsed 2", + "row": 1498 + }, + { + "type": "FieldMismatch", + "code": "TooFewFields", + "message": "Too few fields: expected 3 fields but parsed 2", + "row": 1998 + } + ]); + assert.strictEqual(2000, parsedCsv.data.length); + done(); + }, + }); + }); + + it('piped streaming CSV should be correctly parsed', function(done) { + var data = []; + var readStream = fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'); + var csvStream = readStream.pipe(Papa.parse(Papa.NODE_STREAM_INPUT)); + csvStream.on('data', function(item) { + data.push(item); + }); + csvStream.on('end', function() { + assert.deepEqual(data[0], [ + 'Grant', + 'Dyer', + 'Donec.elementum@orciluctuset.example', + '2013-11-23T02:30:31-08:00', + '2014-05-31T01:06:56-07:00', + 'Magna Ut Associates', + 'ljenkins' + ]); + assert.deepEqual(data[7], [ + 'Talon', + 'Salinas', + 'posuere.vulputate.lacus@Donecsollicitudin.example', + '2015-01-31T09:19:02-08:00', + '2014-12-17T04:59:18-08:00', + 'Aliquam Iaculis Incorporate', + 'Phasellus@Quisquetincidunt.example' + ]); + done(); + }); + }); + + + it('piped streaming CSV should be correctly parsed when header is true', function(done) { + var data = []; + var readStream = fs.createReadStream(__dirname + '/sample-header.csv', 'utf8'); + var csvStream = readStream.pipe(Papa.parse(Papa.NODE_STREAM_INPUT, {header: true})); + csvStream.on('data', function(item) { + data.push(item); + }); + csvStream.on('end', function() { + assert.deepEqual(data[0], { title: 'test title 01', name: 'test name 01' }); + assert.deepEqual(data[1], { title: '', name: 'test name 02' }); + done(); + }); + }); + + it('should support pausing and resuming on same tick when streaming', function(done) { + var rows = []; + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + chunk: function(results, parser) { + rows = rows.concat(results.data); + parser.pause(); + parser.resume(); + }, + error: function(err) { + done(new Error(err)); + }, + complete: function() { + assert.deepEqual(rows[0], [ + 'Grant', + 'Dyer', + 'Donec.elementum@orciluctuset.example', + '2013-11-23T02:30:31-08:00', + '2014-05-31T01:06:56-07:00', + 'Magna Ut Associates', + 'ljenkins' + ]); + assert.deepEqual(rows[7], [ + 'Talon', + 'Salinas', + 'posuere.vulputate.lacus@Donecsollicitudin.example', + '2015-01-31T09:19:02-08:00', + '2014-12-17T04:59:18-08:00', + 'Aliquam Iaculis Incorporate', + 'Phasellus@Quisquetincidunt.example' + ]); + done(); + } + }); + }); + + it('should support pausing and resuming asynchronously when streaming', function(done) { + var rows = []; + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + chunk: function(results, parser) { + rows = rows.concat(results.data); + parser.pause(); + setTimeout(function() { + parser.resume(); + }, 200); + }, + error: function(err) { + done(new Error(err)); + }, + complete: function() { + assert.deepEqual(rows[0], [ + 'Grant', + 'Dyer', + 'Donec.elementum@orciluctuset.example', + '2013-11-23T02:30:31-08:00', + '2014-05-31T01:06:56-07:00', + 'Magna Ut Associates', + 'ljenkins' + ]); + assert.deepEqual(rows[7], [ + 'Talon', + 'Salinas', + 'posuere.vulputate.lacus@Donecsollicitudin.example', + '2015-01-31T09:19:02-08:00', + '2014-12-17T04:59:18-08:00', + 'Aliquam Iaculis Incorporate', + 'Phasellus@Quisquetincidunt.example' + ]); + done(); + } + }); + }); + + it('handles errors in beforeFirstChunk', function(done) { + var expectedError = new Error('test'); + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + beforeFirstChunk: function() { + throw expectedError; + }, + error: function(err) { + assert.deepEqual(err, expectedError); + done(); + } + }); + }); + + it('handles errors in chunk', function(done) { + var expectedError = new Error('test'); + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + chunk: function() { + throw expectedError; + }, + error: function(err) { + assert.deepEqual(err, expectedError); + done(); + } + }); + }); + + it('handles errors in step', function(done) { + var expectedError = new Error('test'); + Papa.parse(fs.createReadStream(__dirname + '/long-sample.csv', 'utf8'), { + step: function() { + throw expectedError; + }, + error: function(err) { + assert.deepEqual(err, expectedError); + done(); + } + }); + }); + + it('handles utf-8 BOM encoded files', function(done) { + Papa.parse(utf8BomSampleRawCsv, { + header: true, + complete: function(parsedCsv) { + assert.deepEqual(parsedCsv.data[0], { A: 'X', B: 'Y', C: 'Z' }); + done(); + } + }); + }); +}); diff --git a/node_modules/papaparse/tests/sample-header.csv b/node_modules/papaparse/tests/sample-header.csv new file mode 100644 index 0000000..bc2472b --- /dev/null +++ b/node_modules/papaparse/tests/sample-header.csv @@ -0,0 +1,3 @@ +title,name +test title 01,test name 01 +,test name 02 diff --git a/node_modules/papaparse/tests/sample.csv b/node_modules/papaparse/tests/sample.csv new file mode 100644 index 0000000..0930f8f --- /dev/null +++ b/node_modules/papaparse/tests/sample.csv @@ -0,0 +1,2 @@ +A,B,C +X,Y,Z \ No newline at end of file diff --git a/node_modules/papaparse/tests/test-cases.js b/node_modules/papaparse/tests/test-cases.js new file mode 100644 index 0000000..0243a2c --- /dev/null +++ b/node_modules/papaparse/tests/test-cases.js @@ -0,0 +1,2744 @@ +var chai; +var Papa; +if (typeof module !== 'undefined' && module.exports) { + chai = require('chai'); + Papa = require('../papaparse.js'); +} + +var assert = chai.assert; + +var BASE_PATH = (typeof document === 'undefined') ? './' : document.getElementById('test-cases').src.replace(/test-cases\.js$/, ''); +var RECORD_SEP = String.fromCharCode(30); +var UNIT_SEP = String.fromCharCode(31); +var FILES_ENABLED = false; +try { + new File([""], ""); // eslint-disable-line no-new + FILES_ENABLED = true; +} catch (e) {} // safari, ie + +var XHR_ENABLED = false; +try { + new XMLHttpRequest(); // eslint-disable-line no-new + XHR_ENABLED = true; +} catch (e) {} // safari, ie + +// Tests for the core parser using new Papa.Parser().parse() (CSV to JSON) +var CORE_PARSER_TESTS = [ + { + description: "One row", + input: 'A,b,c', + expected: { + data: [['A', 'b', 'c']], + errors: [] + } + }, + { + description: "Two rows", + input: 'A,b,c\nd,E,f', + expected: { + data: [['A', 'b', 'c'], ['d', 'E', 'f']], + errors: [] + } + }, + { + description: "Three rows", + input: 'A,b,c\nd,E,f\nG,h,i', + expected: { + data: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']], + errors: [] + } + }, + { + description: "Whitespace at edges of unquoted field", + input: 'a, b ,c', + notes: "Extra whitespace should graciously be preserved", + expected: { + data: [['a', ' b ', 'c']], + errors: [] + } + }, + { + description: "Quoted field", + input: 'A,"B",C', + expected: { + data: [['A', 'B', 'C']], + errors: [] + } + }, + { + description: "Quoted field with extra whitespace on edges", + input: 'A," B ",C', + expected: { + data: [['A', ' B ', 'C']], + errors: [] + } + }, + { + description: "Quoted field with delimiter", + input: 'A,"B,B",C', + expected: { + data: [['A', 'B,B', 'C']], + errors: [] + } + }, + { + description: "Quoted field with line break", + input: 'A,"B\nB",C', + expected: { + data: [['A', 'B\nB', 'C']], + errors: [] + } + }, + { + description: "Quoted fields with line breaks", + input: 'A,"B\nB","C\nC\nC"', + expected: { + data: [['A', 'B\nB', 'C\nC\nC']], + errors: [] + } + }, + { + description: "Quoted fields at end of row with delimiter and line break", + input: 'a,b,"c,c\nc"\nd,e,f', + expected: { + data: [['a', 'b', 'c,c\nc'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Quoted field with escaped quotes", + input: 'A,"B""B""B",C', + expected: { + data: [['A', 'B"B"B', 'C']], + errors: [] + } + }, + { + description: "Quoted field with escaped quotes at boundaries", + input: 'A,"""B""",C', + expected: { + data: [['A', '"B"', 'C']], + errors: [] + } + }, + { + description: "Unquoted field with quotes at end of field", + notes: "The quotes character is misplaced, but shouldn't generate an error or break the parser", + input: 'A,B",C', + expected: { + data: [['A', 'B"', 'C']], + errors: [] + } + }, + { + description: "Quoted field with quotes around delimiter", + input: 'A,""",""",C', + notes: "For a boundary to exist immediately before the quotes, we must not already be in quotes", + expected: { + data: [['A', '","', 'C']], + errors: [] + } + }, + { + description: "Quoted field with quotes on right side of delimiter", + input: 'A,",""",C', + notes: "Similar to the test above but with quotes only after the comma", + expected: { + data: [['A', ',"', 'C']], + errors: [] + } + }, + { + description: "Quoted field with quotes on left side of delimiter", + input: 'A,""",",C', + notes: "Similar to the test above but with quotes only before the comma", + expected: { + data: [['A', '",', 'C']], + errors: [] + } + }, + { + description: "Quoted field with 5 quotes in a row and a delimiter in there, too", + input: '"1","cnonce="""",nc=""""","2"', + notes: "Actual input reported in issue #121", + expected: { + data: [['1', 'cnonce="",nc=""', '2']], + errors: [] + } + }, + { + description: "Quoted field with whitespace around quotes", + input: 'A, "B" ,C', + notes: "The quotes must be immediately adjacent to the delimiter to indicate a quoted field", + expected: { + data: [['A', ' "B" ', 'C']], + errors: [] + } + }, + { + description: "Misplaced quotes in data, not as opening quotes", + input: 'A,B "B",C', + notes: "The input is technically malformed, but this syntax should not cause an error", + expected: { + data: [['A', 'B "B"', 'C']], + errors: [] + } + }, + { + description: "Quoted field has no closing quote", + input: 'a,"b,c\nd,e,f', + expected: { + data: [['a', 'b,c\nd,e,f']], + errors: [{ + "type": "Quotes", + "code": "MissingQuotes", + "message": "Quoted field unterminated", + "row": 0, + "index": 3 + }] + } + }, + { + description: "Quoted field has invalid trailing quote after delimiter with a valid closer", + input: '"a,"b,c"\nd,e,f', + notes: "The input is malformed, opening quotes identified, trailing quote is malformed. Trailing quote should be escaped or followed by valid new line or delimiter to be valid", + expected: { + data: [['a,"b,c'], ['d', 'e', 'f']], + errors: [{ + "type": "Quotes", + "code": "InvalidQuotes", + "message": "Trailing quote on quoted field is malformed", + "row": 0, + "index": 1 + }] + } + }, + { + description: "Quoted field has invalid trailing quote after delimiter", + input: 'a,"b,"c\nd,e,f', + notes: "The input is malformed, opening quotes identified, trailing quote is malformed. Trailing quote should be escaped or followed by valid new line or delimiter to be valid", + expected: { + data: [['a', 'b,"c\nd,e,f']], + errors: [{ + "type": "Quotes", + "code": "InvalidQuotes", + "message": "Trailing quote on quoted field is malformed", + "row": 0, + "index": 3 + }, + { + "type": "Quotes", + "code": "MissingQuotes", + "message": "Quoted field unterminated", + "row": 0, + "index": 3 + }] + } + }, + { + description: "Quoted field has invalid trailing quote before delimiter", + input: 'a,"b"c,d\ne,f,g', + notes: "The input is malformed, opening quotes identified, trailing quote is malformed. Trailing quote should be escaped or followed by valid new line or delimiter to be valid", + expected: { + data: [['a', 'b"c,d\ne,f,g']], + errors: [{ + "type": "Quotes", + "code": "InvalidQuotes", + "message": "Trailing quote on quoted field is malformed", + "row": 0, + "index": 3 + }, + { + "type": "Quotes", + "code": "MissingQuotes", + "message": "Quoted field unterminated", + "row": 0, + "index": 3 + }] + } + }, + { + description: "Quoted field has invalid trailing quote after new line", + input: 'a,"b,c\nd"e,f,g', + notes: "The input is malformed, opening quotes identified, trailing quote is malformed. Trailing quote should be escaped or followed by valid new line or delimiter to be valid", + expected: { + data: [['a', 'b,c\nd"e,f,g']], + errors: [{ + "type": "Quotes", + "code": "InvalidQuotes", + "message": "Trailing quote on quoted field is malformed", + "row": 0, + "index": 3 + }, + { + "type": "Quotes", + "code": "MissingQuotes", + "message": "Quoted field unterminated", + "row": 0, + "index": 3 + }] + } + }, + { + description: "Quoted field has valid trailing quote via delimiter", + input: 'a,"b",c\nd,e,f', + notes: "Trailing quote is valid due to trailing delimiter", + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Quoted field has valid trailing quote via \\n", + input: 'a,b,"c"\nd,e,f', + notes: "Trailing quote is valid due to trailing new line delimiter", + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Quoted field has valid trailing quote via EOF", + input: 'a,b,c\nd,e,"f"', + notes: "Trailing quote is valid due to EOF", + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Quoted field contains delimiters and \\n with valid trailing quote", + input: 'a,"b,c\nd,e,f"', + notes: "Trailing quote is valid due to trailing delimiter", + expected: { + data: [['a', 'b,c\nd,e,f']], + errors: [] + } + }, + { + description: "Line starts with quoted field", + input: 'a,b,c\n"d",e,f', + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Line starts with unquoted empty field", + input: ',b,c\n"d",e,f', + expected: { + data: [['', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Line ends with quoted field", + input: 'a,b,c\nd,e,f\n"g","h","i"\n"j","k","l"', + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'], ['j', 'k', 'l']], + errors: [] + } + }, + { + description: "Line ends with quoted field, first field of next line is empty, \\n", + input: 'a,b,c\n,e,f\n,"h","i"\n,"k","l"', + config: { + newline: '\n', + }, + expected: { + data: [['a', 'b', 'c'], ['', 'e', 'f'], ['', 'h', 'i'], ['', 'k', 'l']], + errors: [] + } + }, + { + description: "Quoted field at end of row (but not at EOF) has quotes", + input: 'a,b,"c""c"""\nd,e,f', + expected: { + data: [['a', 'b', 'c"c"'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Empty quoted field at EOF is empty", + input: 'a,b,""\na,b,""', + expected: { + data: [['a', 'b', ''], ['a', 'b', '']], + errors: [] + } + }, + { + description: "Multiple consecutive empty fields", + input: 'a,b,,,c,d\n,,e,,,f', + expected: { + data: [['a', 'b', '', '', 'c', 'd'], ['', '', 'e', '', '', 'f']], + errors: [] + } + }, + { + description: "Empty input string", + input: '', + expected: { + data: [], + errors: [] + } + }, + { + description: "Input is just the delimiter (2 empty fields)", + input: ',', + expected: { + data: [['', '']], + errors: [] + } + }, + { + description: "Input is just empty fields", + input: ',,\n,,,', + expected: { + data: [['', '', ''], ['', '', '', '']], + errors: [] + } + }, + { + description: "Input is just a string (a single field)", + input: 'Abc def', + expected: { + data: [['Abc def']], + errors: [] + } + }, + { + description: "Commented line at beginning", + input: '# Comment!\na,b,c', + config: { comments: true }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Commented line in middle", + input: 'a,b,c\n# Comment\nd,e,f', + config: { comments: true }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Commented line at end", + input: 'a,true,false\n# Comment', + config: { comments: true }, + expected: { + data: [['a', 'true', 'false']], + errors: [] + } + }, + { + description: "Two comment lines consecutively", + input: 'a,b,c\n#comment1\n#comment2\nd,e,f', + config: { comments: true }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Two comment lines consecutively at end of file", + input: 'a,b,c\n#comment1\n#comment2', + config: { comments: true }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Three comment lines consecutively at beginning of file", + input: '#comment1\n#comment2\n#comment3\na,b,c', + config: { comments: true }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Entire file is comment lines", + input: '#comment1\n#comment2\n#comment3', + config: { comments: true }, + expected: { + data: [], + errors: [] + } + }, + { + description: "Comment with non-default character", + input: 'a,b,c\n!Comment goes here\nd,e,f', + config: { comments: '!' }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Bad comments value specified", + notes: "Should silently disable comment parsing", + input: 'a,b,c\n5comment\nd,e,f', + config: { comments: 5 }, + expected: { + data: [['a', 'b', 'c'], ['5comment'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Multi-character comment string", + input: 'a,b,c\n=N(Comment)\nd,e,f', + config: { comments: "=N(" }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Input with only a commented line", + input: '#commented line', + config: { comments: true, delimiter: ',' }, + expected: { + data: [], + errors: [] + } + }, + { + description: "Input with only a commented line and blank line after", + input: '#commented line\n', + config: { comments: true, delimiter: ',' }, + expected: { + data: [['']], + errors: [] + } + }, + { + description: "Input with only a commented line, without comments enabled", + input: '#commented line', + config: { delimiter: ',' }, + expected: { + data: [['#commented line']], + errors: [] + } + }, + { + description: "Input without comments with line starting with whitespace", + input: 'a\n b\nc', + config: { delimiter: ',' }, + notes: "\" \" == false, but \" \" !== false, so === comparison is required", + expected: { + data: [['a'], [' b'], ['c']], + errors: [] + } + }, + { + description: "Multiple rows, one column (no delimiter found)", + input: 'a\nb\nc\nd\ne', + expected: { + data: [['a'], ['b'], ['c'], ['d'], ['e']], + errors: [] + } + }, + { + description: "One column input with empty fields", + input: 'a\nb\n\n\nc\nd\ne\n', + expected: { + data: [['a'], ['b'], [''], [''], ['c'], ['d'], ['e'], ['']], + errors: [] + } + }, + { + description: "Fast mode, basic", + input: 'a,b,c\nd,e,f', + config: { fastMode: true }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Fast mode with comments", + input: '// Commented line\na,b,c', + config: { fastMode: true, comments: "//" }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Fast mode with preview", + input: 'a,b,c\nd,e,f\nh,j,i\n', + config: { fastMode: true, preview: 2 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Fast mode with blank line at end", + input: 'a,b,c\n', + config: { fastMode: true }, + expected: { + data: [['a', 'b', 'c'], ['']], + errors: [] + } + }, + { + description: "Simple duplicate header names", + input: 'A,A,A,A\n1,2,3,4', + config: { header: true }, + expected: { + data: [['A', 'A_1', 'A_2', 'A_3'], ['1', '2', '3', '4']], + errors: [] + } + }, + { + description: "Duplicate header names with headerTransform", + input: 'A,A,A,A\n1,2,3,4', + config: { header: true, transformHeader: function(header) { return header.toLowerCase(); } }, + expected: { + data: [['a', 'a_1', 'a_2', 'a_3'], ['1', '2', '3', '4']], + errors: [] + } + }, + { + description: "Duplicate header names existing column", + input: 'c,c,c,c_1\n1,2,3,4', + config: { header: true }, + expected: { + data: [['c', 'c_1', 'c_2', 'c_1_0'], ['1', '2', '3', '4']], + errors: [] + } + }, +]; + +describe('Core Parser Tests', function() { + function generateTest(test) { + (test.disabled ? it.skip : it)(test.description, function() { + var actual = new Papa.Parser(test.config).parse(test.input); + assert.deepEqual(actual.errors, test.expected.errors); + assert.deepEqual(actual.data, test.expected.data); + }); + } + + for (var i = 0; i < CORE_PARSER_TESTS.length; i++) { + generateTest(CORE_PARSER_TESTS[i]); + } +}); + + + +// Tests for Papa.parse() function -- high-level wrapped parser (CSV to JSON) +var PARSE_TESTS = [ + { + description: "Two rows, just \\r", + input: 'A,b,c\rd,E,f', + expected: { + data: [['A', 'b', 'c'], ['d', 'E', 'f']], + errors: [] + } + }, + { + description: "Two rows, \\r\\n", + input: 'A,b,c\r\nd,E,f', + expected: { + data: [['A', 'b', 'c'], ['d', 'E', 'f']], + errors: [] + } + }, + { + description: "Quoted field with \\r\\n", + input: 'A,"B\r\nB",C', + expected: { + data: [['A', 'B\r\nB', 'C']], + errors: [] + } + }, + { + description: "Quoted field with \\r", + input: 'A,"B\rB",C', + expected: { + data: [['A', 'B\rB', 'C']], + errors: [] + } + }, + { + description: "Quoted field with \\n", + input: 'A,"B\nB",C', + expected: { + data: [['A', 'B\nB', 'C']], + errors: [] + } + }, + { + description: "Quoted fields with spaces between closing quote and next delimiter", + input: 'A,"B" ,C,D\r\nE,F,"G" ,H', + expected: { + data: [['A', 'B', 'C','D'],['E', 'F', 'G','H']], + errors: [] + } + }, + { + description: "Quoted fields with spaces between closing quote and next new line", + input: 'A,B,C,"D" \r\nE,F,G,"H" \r\nQ,W,E,R', + expected: { + data: [['A', 'B', 'C','D'],['E', 'F', 'G','H'],['Q', 'W', 'E','R']], + errors: [] + } + }, + { + description: "Quoted fields with spaces after closing quote", + input: 'A,"B" ,C,"D" \r\nE,F,"G" ,"H" \r\nQ,W,"E" ,R', + expected: { + data: [['A', 'B', 'C','D'],['E', 'F', 'G','H'],['Q', 'W', 'E','R']], + errors: [] + } + }, + { + description: "Misplaced quotes in data twice, not as opening quotes", + input: 'A,B",C\nD,E",F', + expected: { + data: [['A', 'B"', 'C'], ['D', 'E"', 'F']], + errors: [] + } + }, + { + description: "Mixed slash n and slash r should choose first as precident", + input: 'a,b,c\nd,e,f\rg,h,i\n', + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f\rg', 'h', 'i'], ['']], + errors: [] + } + }, + { + description: "Header row with one row of data", + input: 'A,B,C\r\na,b,c', + config: { header: true }, + expected: { + data: [{"A": "a", "B": "b", "C": "c"}], + errors: [] + } + }, + { + description: "Header row only", + input: 'A,B,C', + config: { header: true }, + expected: { + data: [], + errors: [] + } + }, + { + description: "Row with too few fields", + input: 'A,B,C\r\na,b', + config: { header: true }, + expected: { + data: [{"A": "a", "B": "b"}], + errors: [{ + "type": "FieldMismatch", + "code": "TooFewFields", + "message": "Too few fields: expected 3 fields but parsed 2", + "row": 0 + }] + } + }, + { + description: "Row with too many fields", + input: 'A,B,C\r\na,b,c,d,e\r\nf,g,h', + config: { header: true }, + expected: { + data: [{"A": "a", "B": "b", "C": "c", "__parsed_extra": ["d", "e"]}, {"A": "f", "B": "g", "C": "h"}], + errors: [{ + "type": "FieldMismatch", + "code": "TooManyFields", + "message": "Too many fields: expected 3 fields but parsed 5", + "row": 0 + }] + } + }, + { + description: "Row with enough fields but blank field in the begining", + input: 'A,B,C\r\n,b1,c1\r\na2,b2,c2', + expected: { + data: [["A", "B", "C"], ['', 'b1', 'c1'], ['a2', 'b2', 'c2']], + errors: [] + } + }, + { + description: "Row with enough fields but blank field in the begining using headers", + input: 'A,B,C\r\n,b1,c1\r\n,b2,c2', + config: { header: true }, + expected: { + data: [{"A": "", "B": "b1", "C": "c1"}, {"A": "", "B": "b2", "C": "c2"}], + errors: [] + } + }, + { + description: "Row with enough fields but blank field at end", + input: 'A,B,C\r\na,b,', + config: { header: true }, + expected: { + data: [{"A": "a", "B": "b", "C": ""}], + errors: [] + } + }, + { + description: "Header rows are transformed when transformHeader function is provided", + input: 'A,B,C\r\na,b,c', + config: { header: true, transformHeader: function(header) { return header.toLowerCase(); } }, + expected: { + data: [{"a": "a", "b": "b", "c": "c"}], + errors: [] + } + }, + { + description: "transformHeader accepts and optional index attribute", + input: 'A,B,C\r\na,b,c', + config: { header: true, transformHeader: function(header, i) { return i % 2 ? header.toLowerCase() : header; } }, + expected: { + data: [{"A": "a", "b": "b", "C": "c"}], + errors: [] + } + }, + { + description: "Line ends with quoted field, first field of next line is empty using headers", + input: 'a,b,"c"\r\nd,e,"f"\r\n,"h","i"\r\n,"k","l"', + config: { + header: true, + newline: '\r\n', + }, + expected: { + data: [ + {a: 'd', b: 'e', c: 'f'}, + {a: '', b: 'h', c: 'i'}, + {a: '', b: 'k', c: 'l'} + ], + errors: [] + } + }, + { + description: "Tab delimiter", + input: 'a\tb\tc\r\nd\te\tf', + config: { delimiter: "\t" }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Pipe delimiter", + input: 'a|b|c\r\nd|e|f', + config: { delimiter: "|" }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "ASCII 30 delimiter", + input: 'a' + RECORD_SEP + 'b' + RECORD_SEP + 'c\r\nd' + RECORD_SEP + 'e' + RECORD_SEP + 'f', + config: { delimiter: RECORD_SEP }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "ASCII 31 delimiter", + input: 'a' + UNIT_SEP + 'b' + UNIT_SEP + 'c\r\nd' + UNIT_SEP + 'e' + UNIT_SEP + 'f', + config: { delimiter: UNIT_SEP }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Bad delimiter (\\n)", + input: 'a,b,c', + config: { delimiter: "\n" }, + notes: "Should silently default to comma", + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Multi-character delimiter", + input: 'a, b, c', + config: { delimiter: ", " }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Multi-character delimiter (length 2) with quoted field", + input: 'a, b, "c, e", d', + config: { delimiter: ", " }, + notes: "The quotes must be immediately adjacent to the delimiter to indicate a quoted field", + expected: { + data: [['a', 'b', 'c, e', 'd']], + errors: [] + } + }, + { + description: "Callback delimiter", + input: 'a$ b$ c', + config: { delimiter: function(input) { return input[1] + ' '; } }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Dynamic typing converts numeric literals and maintains precision", + input: '1,2.2,1e3\r\n-4,-4.5,-4e-5\r\n-,5a,5-2\r\n16142028098527942586,9007199254740991,-9007199254740992', + config: { dynamicTyping: true }, + expected: { + data: [[1, 2.2, 1000], [-4, -4.5, -0.00004], ["-", "5a", "5-2"], ["16142028098527942586", 9007199254740991, "-9007199254740992"]], + errors: [] + } + }, + { + description: "Dynamic typing converts boolean literals", + input: 'true,false,T,F,TRUE,FALSE,True,False', + config: { dynamicTyping: true }, + expected: { + data: [[true, false, "T", "F", true, false, "True", "False"]], + errors: [] + } + }, + { + description: "Dynamic typing doesn't convert other types", + input: 'A,B,C\r\nundefined,null,[\r\nvar,float,if', + config: { dynamicTyping: true }, + expected: { + data: [["A", "B", "C"], ["undefined", "null", "["], ["var", "float", "if"]], + errors: [] + } + }, + { + description: "Dynamic typing applies to specific columns", + input: 'A,B,C\r\n1,2.2,1e3\r\n-4,-4.5,-4e-5', + config: { header: true, dynamicTyping: { A: true, C: true } }, + expected: { + data: [{"A": 1, "B": "2.2", "C": 1000}, {"A": -4, "B": "-4.5", "C": -0.00004}], + errors: [] + } + }, + { + description: "Dynamic typing applies to specific columns by index", + input: '1,2.2,1e3\r\n-4,-4.5,-4e-5\r\n-,5a,5-2', + config: { dynamicTyping: { 1: true } }, + expected: { + data: [["1", 2.2, "1e3"], ["-4", -4.5, "-4e-5"], ["-", "5a", "5-2"]], + errors: [] + } + }, + { + description: "Dynamic typing can be applied to `__parsed_extra`", + input: 'A,B,C\r\n1,2.2,1e3,5.5\r\n-4,-4.5,-4e-5', + config: { header: true, dynamicTyping: { A: true, C: true, __parsed_extra: true } }, + expected: { + data: [{"A": 1, "B": "2.2", "C": 1000, "__parsed_extra": [5.5]}, {"A": -4, "B": "-4.5", "C": -0.00004}], + errors: [{ + "type": "FieldMismatch", + "code": "TooManyFields", + "message": "Too many fields: expected 3 fields but parsed 4", + "row": 0 + }] + } + }, + { + description: "Dynamic typing by indices can be determined by function", + input: '001,002,003', + config: { dynamicTyping: function(field) { return (field % 2) === 0; } }, + expected: { + data: [[1, "002", 3]], + errors: [] + } + }, + { + description: "Dynamic typing by headers can be determined by function", + input: 'A_as_int,B,C_as_int\r\n001,002,003', + config: { header: true, dynamicTyping: function(field) { return /_as_int$/.test(field); } }, + expected: { + data: [{"A_as_int": 1, "B": "002", "C_as_int": 3}], + errors: [] + } + }, + { + description: "Dynamic typing converts empty values into NULL", + input: '1,2.2,1e3\r\n,NULL,\r\n-,5a,null', + config: { dynamicTyping: true }, + expected: { + data: [[1, 2.2, 1000], [null, "NULL", null], ["-", "5a", "null"]], + errors: [] + } + }, + { + description: "Custom transform function is applied to values", + input: 'A,B,C\r\nd,e,f', + config: { + transform: function(value) { + return value.toLowerCase(); + } + }, + expected: { + data: [["a","b","c"], ["d","e","f"]], + errors: [] + } + }, + { + description: "Custom transform accepts column number also", + input: 'A,B,C\r\nd,e,f', + config: { + transform: function(value, column) { + if (column % 2) { + value = value.toLowerCase(); + } + return value; + } + }, + expected: { + data: [["A","b","C"], ["d","e","f"]], + errors: [] + } + }, + { + description: "Custom transform accepts header name when using header", + input: 'A,B,C\r\nd,e,f', + config: { + header: true, + transform: function(value, name) { + if (name === 'B') { + value = value.toUpperCase(); + } + return value; + } + }, + expected: { + data: [{'A': "d", 'B': "E", 'C': "f"}], + errors: [] + } + }, + { + description: "Dynamic typing converts ISO date strings to Dates", + input: 'ISO date,long date\r\n2018-05-04T21:08:03.269Z,Fri May 04 2018 14:08:03 GMT-0700 (PDT)\r\n2018-05-08T15:20:22.642Z,Tue May 08 2018 08:20:22 GMT-0700 (PDT)', + config: { dynamicTyping: true }, + expected: { + data: [["ISO date", "long date"], [new Date("2018-05-04T21:08:03.269Z"), "Fri May 04 2018 14:08:03 GMT-0700 (PDT)"], [new Date("2018-05-08T15:20:22.642Z"), "Tue May 08 2018 08:20:22 GMT-0700 (PDT)"]], + errors: [] + } + }, + { + description: "Dynamic typing skips ISO date strings ocurring in other strings", + input: 'ISO date,String with ISO date\r\n2018-05-04T21:08:03.269Z,The date is 2018-05-04T21:08:03.269Z\r\n2018-05-08T15:20:22.642Z,The date is 2018-05-08T15:20:22.642Z', + config: { dynamicTyping: true }, + expected: { + data: [["ISO date", "String with ISO date"], [new Date("2018-05-04T21:08:03.269Z"), "The date is 2018-05-04T21:08:03.269Z"], [new Date("2018-05-08T15:20:22.642Z"), "The date is 2018-05-08T15:20:22.642Z"]], + errors: [] + } + }, + { + description: "Blank line at beginning", + input: '\r\na,b,c\r\nd,e,f', + config: { newline: '\r\n' }, + expected: { + data: [[''], ['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Blank line in middle", + input: 'a,b,c\r\n\r\nd,e,f', + config: { newline: '\r\n' }, + expected: { + data: [['a', 'b', 'c'], [''], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Blank lines at end", + input: 'a,b,c\nd,e,f\n\n', + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f'], [''], ['']], + errors: [] + } + }, + { + description: "Blank line in middle with whitespace", + input: 'a,b,c\r\n \r\nd,e,f', + expected: { + data: [['a', 'b', 'c'], [" "], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "First field of a line is empty", + input: 'a,b,c\r\n,e,f', + expected: { + data: [['a', 'b', 'c'], ['', 'e', 'f']], + errors: [] + } + }, + { + description: "Last field of a line is empty", + input: 'a,b,\r\nd,e,f', + expected: { + data: [['a', 'b', ''], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Other fields are empty", + input: 'a,,c\r\n,,', + expected: { + data: [['a', '', 'c'], ['', '', '']], + errors: [] + } + }, + { + description: "Empty input string", + input: '', + expected: { + data: [], + errors: [{ + "type": "Delimiter", + "code": "UndetectableDelimiter", + "message": "Unable to auto-detect delimiting character; defaulted to ','" + }] + } + }, + { + description: "Input is just the delimiter (2 empty fields)", + input: ',', + expected: { + data: [['', '']], + errors: [] + } + }, + { + description: "Input is just a string (a single field)", + input: 'Abc def', + expected: { + data: [['Abc def']], + errors: [ + { + "type": "Delimiter", + "code": "UndetectableDelimiter", + "message": "Unable to auto-detect delimiting character; defaulted to ','" + } + ] + } + }, + { + description: "Preview 0 rows should default to parsing all", + input: 'a,b,c\r\nd,e,f\r\ng,h,i', + config: { preview: 0 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']], + errors: [] + } + }, + { + description: "Preview 1 row", + input: 'a,b,c\r\nd,e,f\r\ng,h,i', + config: { preview: 1 }, + expected: { + data: [['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Preview 2 rows", + input: 'a,b,c\r\nd,e,f\r\ng,h,i', + config: { preview: 2 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Preview all (3) rows", + input: 'a,b,c\r\nd,e,f\r\ng,h,i', + config: { preview: 3 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']], + errors: [] + } + }, + { + description: "Preview more rows than input has", + input: 'a,b,c\r\nd,e,f\r\ng,h,i', + config: { preview: 4 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']], + errors: [] + } + }, + { + description: "Preview should count rows, not lines", + input: 'a,b,c\r\nd,e,"f\r\nf",g,h,i', + config: { preview: 2 }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f\r\nf', 'g', 'h', 'i']], + errors: [] + } + }, + { + description: "Preview with header row", + notes: "Preview is defined to be number of rows of input not including header row", + input: 'a,b,c\r\nd,e,f\r\ng,h,i\r\nj,k,l', + config: { header: true, preview: 2 }, + expected: { + data: [{"a": "d", "b": "e", "c": "f"}, {"a": "g", "b": "h", "c": "i"}], + errors: [] + } + }, + { + description: "Empty lines", + input: '\na,b,c\n\nd,e,f\n\n', + config: { delimiter: ',' }, + expected: { + data: [[''], ['a', 'b', 'c'], [''], ['d', 'e', 'f'], [''], ['']], + errors: [] + } + }, + { + description: "Skip empty lines", + input: 'a,b,c\n\nd,e,f', + config: { skipEmptyLines: true }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Skip empty lines, with newline at end of input", + input: 'a,b,c\r\n\r\nd,e,f\r\n', + config: { skipEmptyLines: true }, + expected: { + data: [['a', 'b', 'c'], ['d', 'e', 'f']], + errors: [] + } + }, + { + description: "Skip empty lines, with empty input", + input: '', + config: { skipEmptyLines: true }, + expected: { + data: [], + errors: [ + { + "type": "Delimiter", + "code": "UndetectableDelimiter", + "message": "Unable to auto-detect delimiting character; defaulted to ','" + } + ] + } + }, + { + description: "Skip empty lines, with first line only whitespace", + notes: "A line must be absolutely empty to be considered empty", + input: ' \na,b,c', + config: { skipEmptyLines: true, delimiter: ',' }, + expected: { + data: [[" "], ['a', 'b', 'c']], + errors: [] + } + }, + { + description: "Skip empty lines while detecting delimiter", + notes: "Parsing correctly newline-terminated short data with delimiter:auto and skipEmptyLines:true", + input: 'a,b\n1,2\n3,4\n', + config: { header: true, skipEmptyLines: true }, + expected: { + data: [{'a': '1', 'b': '2'}, {'a': '3', 'b': '4'}], + errors: [] + } + }, + { + description: "Lines with comments are not used when guessing the delimiter in an escaped file", + notes: "Guessing the delimiter should work even if there are many lines of comments at the start of the file", + input: '#1\n#2\n#3\n#4\n#5\n#6\n#7\n#8\n#9\n#10\none,"t,w,o",three\nfour,five,six', + config: { comments: '#' }, + expected: { + data: [['one','t,w,o','three'],['four','five','six']], + errors: [] + } + }, + { + description: "Lines with comments are not used when guessing the delimiter in a non-escaped file", + notes: "Guessing the delimiter should work even if there are many lines of comments at the start of the file", + input: '#1\n#2\n#3\n#4\n#5\n#6\n#7\n#8\n#9\n#10\n#11\none,two,three\nfour,five,six', + config: { comments: '#' }, + expected: { + data: [['one','two','three'],['four','five','six']], + errors: [] + } + }, + { + description: "Pipe delimiter is guessed correctly when mixed with comas", + notes: "Guessing the delimiter should work even if there are many lines of comments at the start of the file", + input: 'one|two,two|three\nfour|five,five|six', + config: {}, + expected: { + data: [['one','two,two','three'],['four','five,five','six']], + errors: [] + } + }, + { + description: "Pipe delimiter is guessed correctly choose avgFildCount max one", + notes: "Guessing the delimiter should work choose the min delta one and the max one", + config: {}, + input: 'a,b,c\na,b,c|d|e|f', + expected: { + data: [['a', 'b', 'c'], ['a','b','c|d|e|f']], + errors: [] + } + }, + { + description: "Pipe delimiter is guessed correctly when first field are enclosed in quotes and contain delimiter characters", + notes: "Guessing the delimiter should work if the first field is enclosed in quotes, but others are not", + input: '"Field1,1,1";Field2;"Field3";Field4;Field5;Field6', + config: {}, + expected: { + data: [['Field1,1,1','Field2','Field3', 'Field4', 'Field5', 'Field6']], + errors: [] + } + }, + { + description: "Pipe delimiter is guessed correctly when some fields are enclosed in quotes and contain delimiter characters and escaoped quotes", + notes: "Guessing the delimiter should work even if the first field is not enclosed in quotes, but others are", + input: 'Field1;Field2;"Field,3,""3,3";Field4;Field5;"Field6,6"', + config: {}, + expected: { + data: [['Field1','Field2','Field,3,"3,3', 'Field4', 'Field5', 'Field6,6']], + errors: [] + } + }, + { + description: "Single quote as quote character", + notes: "Must parse correctly when single quote is specified as a quote character", + input: "a,b,'c,d'", + config: { quoteChar: "'" }, + expected: { + data: [['a', 'b', 'c,d']], + errors: [] + } + }, + { + description: "Custom escape character in the middle", + notes: "Must parse correctly if the backslash sign (\\) is configured as a custom escape character", + input: 'a,b,"c\\"d\\"f"', + config: { escapeChar: '\\' }, + expected: { + data: [['a', 'b', 'c"d"f']], + errors: [] + } + }, + { + description: "Custom escape character at the end", + notes: "Must parse correctly if the backslash sign (\\) is configured as a custom escape character and the escaped quote character appears at the end of the column", + input: 'a,b,"c\\"d\\""', + config: { escapeChar: '\\' }, + expected: { + data: [['a', 'b', 'c"d"']], + errors: [] + } + }, + { + description: "Custom escape character not used for escaping", + notes: "Must parse correctly if the backslash sign (\\) is configured as a custom escape character and appears as regular character in the text", + input: 'a,b,"c\\d"', + config: { escapeChar: '\\' }, + expected: { + data: [['a', 'b', 'c\\d']], + errors: [] + } + }, + { + description: "Header row with preceding comment", + notes: "Must parse correctly headers if they are preceded by comments", + input: '#Comment\na,b\nc,d\n', + config: { header: true, comments: '#', skipEmptyLines: true, delimiter: ',' }, + expected: { + data: [{'a': 'c', 'b': 'd'}], + errors: [] + } + }, + { + description: "Carriage return in header inside quotes, with line feed endings", + input: '"a\r\na","b"\n"c","d"\n"e","f"\n"g","h"\n"i","j"', + config: {}, + expected: { + data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [] + } + }, + { + description: "Line feed in header inside quotes, with carriage return + line feed endings", + input: '"a\na","b"\r\n"c","d"\r\n"e","f"\r\n"g","h"\r\n"i","j"', + config: {}, + expected: { + data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [] + } + }, + { + description: "Using \\r\\n endings uses \\r\\n linebreak", + input: 'a,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j', + config: {}, + expected: { + data: [['a', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 23, + aborted: false, + truncated: false + } + } + }, + { + description: "Using \\n endings uses \\n linebreak", + input: 'a,b\nc,d\ne,f\ng,h\ni,j', + config: {}, + expected: { + data: [['a', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\n', + delimiter: ',', + cursor: 19, + aborted: false, + truncated: false + } + } + }, + { + description: "Using \\r\\n endings with \\r\\n in header field uses \\r\\n linebreak", + input: '"a\r\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j', + config: {}, + expected: { + data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 28, + aborted: false, + truncated: false + } + } + }, + { + description: "Using \\r\\n endings with \\n in header field uses \\r\\n linebreak", + input: '"a\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j', + config: {}, + expected: { + data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 27, + aborted: false, + truncated: false + } + } + }, + { + description: "Using \\r\\n endings with \\n in header field with skip empty lines uses \\r\\n linebreak", + input: '"a\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j\r\n', + config: {skipEmptyLines: true}, + expected: { + data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 29, + aborted: false, + truncated: false + } + } + }, + { + description: "Using \\n endings with \\r\\n in header field uses \\n linebreak", + input: '"a\r\na",b\nc,d\ne,f\ng,h\ni,j', + config: {}, + expected: { + data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\n', + delimiter: ',', + cursor: 24, + aborted: false, + truncated: false + } + } + }, + { + description: "Using reserved regex character . as quote character", + input: '.a\na.,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j', + config: { quoteChar: '.' }, + expected: { + data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 27, + aborted: false, + truncated: false + } + } + }, + { + description: "Using reserved regex character | as quote character", + input: '|a\na|,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j', + config: { quoteChar: '|' }, + expected: { + data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']], + errors: [], + meta: { + linebreak: '\r\n', + delimiter: ',', + cursor: 27, + aborted: false, + truncated: false + } + } + }, + { + description: "Parsing with skipEmptyLines set to 'greedy'", + notes: "Must parse correctly without lines with no content", + input: 'a,b\n\n,\nc,d\n , \n""," "\n , \n,,,,\n', + config: { skipEmptyLines: 'greedy' }, + expected: { + data: [['a', 'b'], ['c', 'd']], + errors: [] + } + }, + { + description: "Parsing with skipEmptyLines set to 'greedy' with quotes and delimiters as content", + notes: "Must include lines with escaped delimiters and quotes", + input: 'a,b\n\n,\nc,d\n" , ",","\n""" """,""""""\n\n\n', + config: { skipEmptyLines: 'greedy' }, + expected: { + data: [['a', 'b'], ['c', 'd'], [' , ', ','], ['" "', '""']], + errors: [] + } + }, + { + description: "Quoted fields with spaces between closing quote and next delimiter and contains delimiter", + input: 'A,",B" ,C,D\nE,F,G,H', + expected: { + data: [['A', ',B', 'C', 'D'],['E', 'F', 'G', 'H']], + errors: [] + } + }, + { + description: "Quoted fields with spaces between closing quote and newline and contains newline", + input: 'a,b,"c\n" \nd,e,f', + expected: { + data: [['a', 'b', 'c\n'], ['d', 'e', 'f']], + errors: [] + } + } +]; + +describe('Parse Tests', function() { + function generateTest(test) { + (test.disabled ? it.skip : it)(test.description, function() { + var actual = Papa.parse(test.input, test.config); + // allows for testing the meta object if present in the test + if (test.expected.meta) { + assert.deepEqual(actual.meta, test.expected.meta); + } + assert.deepEqual(actual.errors, test.expected.errors); + assert.deepEqual(actual.data, test.expected.data); + }); + } + + for (var i = 0; i < PARSE_TESTS.length; i++) { + generateTest(PARSE_TESTS[i]); + } +}); + + + +// Tests for Papa.parse() that involve asynchronous operation +var PARSE_ASYNC_TESTS = [ + { + description: "Simple worker", + input: "A,B,C\nX,Y,Z", + config: { + worker: true, + }, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "Simple download", + input: BASE_PATH + "sample.csv", + config: { + download: true + }, + disabled: !XHR_ENABLED, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "Simple download + worker", + input: BASE_PATH + "sample.csv", + config: { + worker: true, + download: true + }, + disabled: !XHR_ENABLED, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "Simple file", + disabled: !FILES_ENABLED, + input: FILES_ENABLED ? new File(["A,B,C\nX,Y,Z"], "sample.csv") : false, + config: { + }, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "Simple file + worker", + disabled: !FILES_ENABLED, + input: FILES_ENABLED ? new File(["A,B,C\nX,Y,Z"], "sample.csv") : false, + config: { + worker: true, + }, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "File with a few regular and lots of empty lines", + disabled: !FILES_ENABLED, + input: FILES_ENABLED ? new File(["A,B,C\nX,Y,Z\n" + new Array(500000).fill(",,").join("\n")], "sample.csv") : false, + config: { + skipEmptyLines: "greedy" + }, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + }, + { + description: "File with a few regular and lots of empty lines + worker", + disabled: !FILES_ENABLED, + input: FILES_ENABLED ? new File(["A,B,C\nX,Y,Z\n" + new Array(500000).fill(",,").join("\n")], "sample.csv") : false, + config: { + worker: true, + skipEmptyLines: "greedy" + }, + expected: { + data: [['A','B','C'],['X','Y','Z']], + errors: [] + } + } +]; + +describe('Parse Async Tests', function() { + function generateTest(test) { + (test.disabled ? it.skip : it)(test.description, function(done) { + var config = test.config; + + config.complete = function(actual) { + assert.deepEqual(actual.errors, test.expected.errors); + assert.deepEqual(actual.data, test.expected.data); + done(); + }; + + config.error = function(err) { + throw err; + }; + + Papa.parse(test.input, config); + }); + } + + for (var i = 0; i < PARSE_ASYNC_TESTS.length; i++) { + generateTest(PARSE_ASYNC_TESTS[i]); + } +}); + + + +// Tests for Papa.unparse() function (JSON to CSV) +var UNPARSE_TESTS = [ + { + description: "A simple row", + notes: "Comma should be default delimiter", + input: [['A', 'b', 'c']], + expected: 'A,b,c' + }, + { + description: "Two rows", + input: [['A', 'b', 'c'], ['d', 'E', 'f']], + expected: 'A,b,c\r\nd,E,f' + }, + { + description: "Data with quotes", + input: [['a', '"b"', 'c'], ['"d"', 'e', 'f']], + expected: 'a,"""b""",c\r\n"""d""",e,f' + }, + { + description: "Data with newlines", + input: [['a', 'b\nb', 'c'], ['d', 'e', 'f\r\nf']], + expected: 'a,"b\nb",c\r\nd,e,"f\r\nf"' + }, + { + description: "Array of objects (header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "With header row, missing a field in a row", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,,f' + }, + { + description: "With header row, with extra field in a row", + notes: "Extra field should be ignored; first object in array dictates header row", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Extra": "g", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "Specifying column names and data separately", + input: { fields: ["Col1", "Col2", "Col3"], data: [["a", "b", "c"], ["d", "e", "f"]] }, + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "Specifying column names only (no data)", + notes: "Papa should add a data property that is an empty array to prevent errors (no copy is made)", + input: { fields: ["Col1", "Col2", "Col3"] }, + expected: 'Col1,Col2,Col3' + }, + { + description: "Specifying data only (no field names), improperly", + notes: "A single array for a single row is wrong, but it can be compensated.
Papa should add empty fields property to prevent errors.", + input: { data: ["abc", "d", "ef"] }, + expected: 'abc,d,ef' + }, + { + description: "Specifying data only (no field names), properly", + notes: "An array of arrays, even if just a single row.
Papa should add empty fields property to prevent errors.", + input: { data: [["a", "b", "c"]] }, + expected: 'a,b,c' + }, + { + description: "Custom delimiter (semicolon)", + input: [['A', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: ';' }, + expected: 'A;b;c\r\nd;e;f' + }, + { + description: "Custom delimiter (tab)", + input: [['Ab', 'cd', 'ef'], ['g', 'h', 'ij']], + config: { delimiter: '\t' }, + expected: 'Ab\tcd\tef\r\ng\th\tij' + }, + { + description: "Custom delimiter (ASCII 30)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: RECORD_SEP }, + expected: 'a' + RECORD_SEP + 'b' + RECORD_SEP + 'c\r\nd' + RECORD_SEP + 'e' + RECORD_SEP + 'f' + }, + { + description: "Custom delimiter (Multi-character)", + input: [['A', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: ', ' }, + expected: 'A, b, c\r\nd, e, f' + }, + { + description: "Custom delimiter (Multi-character), field contains custom delimiter", + input: [['A', 'b', 'c'], ['d', 'e', 'f, g']], + config: { delimiter: ', ' }, + expected: 'A, b, c\r\nd, e, "f, g"' + }, + { + description: "Bad delimiter (\\n)", + notes: "Should default to comma", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: '\n' }, + expected: 'a,b,c\r\nd,e,f' + }, + { + description: "Custom line ending (\\r)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '\r' }, + expected: 'a,b,c\rd,e,f' + }, + { + description: "Custom line ending (\\n)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '\n' }, + expected: 'a,b,c\nd,e,f' + }, + { + description: "Custom, but strange, line ending ($)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '$' }, + expected: 'a,b,c$d,e,f' + }, + { + description: "Force quotes around all fields", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { quotes: true }, + expected: '"a","b","c"\r\n"d","e","f"' + }, + { + description: "Force quotes around all fields (with header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + config: { quotes: true }, + expected: '"Col1","Col2","Col3"\r\n"a","b","c"\r\n"d","e","f"' + }, + { + description: "Force quotes around certain fields only", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { quotes: [true, false, true] }, + expected: '"a",b,"c"\r\n"d",e,"f"' + }, + { + description: "Force quotes around certain fields only (with header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + config: { quotes: [true, false, true] }, + expected: '"Col1",Col2,"Col3"\r\n"a",b,"c"\r\n"d",e,"f"' + }, + { + description: "Force quotes around string fields only", + input: [['a', 'b', 'c'], ['d', 10, true]], + config: { quotes: function(value) { return typeof value === 'string'; } }, + expected: '"a","b","c"\r\n"d",10,true' + }, + { + description: "Force quotes around string fields only (with header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": 10, "Col3": true }], + config: { quotes: function(value) { return typeof value === 'string'; } }, + expected: '"Col1","Col2","Col3"\r\n"a","b","c"\r\n"d",10,true' + }, + { + description: "Empty input", + input: [], + expected: '' + }, + { + description: "Mismatched field counts in rows", + input: [['a', 'b', 'c'], ['d', 'e'], ['f']], + expected: 'a,b,c\r\nd,e\r\nf' + }, + { + description: "JSON null is treated as empty value", + input: [{ "Col1": "a", "Col2": null, "Col3": "c" }], + expected: 'Col1,Col2,Col3\r\na,,c' + }, + { + description: "Custom quote character (single quote)", + input: [['a,d','b','c']], + config: { quoteChar: "'"}, + expected: "'a,d',b,c" + }, + { + description: "Don't print header if header:false option specified", + input: [{"Col1": "a", "Col2": "b", "Col3": "c"}, {"Col1": "d", "Col2": "e", "Col3": "f"}], + config: {header: false}, + expected: 'a,b,c\r\nd,e,f' + }, + { + description: "Date objects are exported in its ISO representation", + input: [{date: new Date("2018-05-04T21:08:03.269Z"), "not a date": 16}, {date: new Date("Tue May 08 2018 08:20:22 GMT-0700 (PDT)"), "not a date": 32}], + expected: 'date,not a date\r\n2018-05-04T21:08:03.269Z,16\r\n2018-05-08T15:20:22.000Z,32' + }, + { + description: "Returns empty rows when empty rows are passed and skipEmptyLines is false", + input: [[null, ' '], [], ['1', '2']], + config: {skipEmptyLines: false}, + expected: '," "\r\n\r\n1,2' + }, + { + description: "Returns without empty rows when skipEmptyLines is true", + input: [[null, ' '], [], ['1', '2']], + config: {skipEmptyLines: true}, + expected: '," "\r\n1,2' + }, + { + description: "Returns without rows with no content when skipEmptyLines is 'greedy'", + input: [[null, ' '], [], ['1', '2']].concat(new Array(500000).fill(['', ''])).concat([['3', '4']]), + config: {skipEmptyLines: 'greedy'}, + expected: '1,2\r\n3,4' + }, + { + description: "Returns empty rows when empty rows are passed and skipEmptyLines is false with headers", + input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}], + config: {skipEmptyLines: false, header: true}, + expected: 'a,b\r\n," "\r\n\r\n1,2' + }, + { + description: "Returns without empty rows when skipEmptyLines is true with headers", + input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}], + config: {skipEmptyLines: true, header: true}, + expected: 'a,b\r\n," "\r\n1,2' + }, + { + description: "Returns without rows with no content when skipEmptyLines is 'greedy' with headers", + input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}], + config: {skipEmptyLines: 'greedy', header: true}, + expected: 'a,b\r\n1,2' + }, + { + description: "Column option used to manually specify keys", + notes: "Should not throw any error when attempting to serialize key not present in object. Columns are different than keys of the first object. When an object is missing a key then the serialized value should be an empty string.", + input: [{a: 1, b: '2'}, {}, {a: 3, d: 'd', c: 4,}], + config: {columns: ['a', 'b', 'c']}, + expected: 'a,b,c\r\n1,2,\r\n\r\n3,,4' + }, + { + description: "Column option used to manually specify keys with input type object", + notes: "Should not throw any error when attempting to serialize key not present in object. Columns are different than keys of the first object. When an object is missing a key then the serialized value should be an empty string.", + input: { data: [{a: 1, b: '2'}, {}, {a: 3, d: 'd', c: 4,}] }, + config: {columns: ['a', 'b', 'c']}, + expected: 'a,b,c\r\n1,2,\r\n\r\n3,,4' + }, + { + description: "Use different escapeChar", + input: [{a: 'foo', b: '"quoted"'}], + config: {header: false, escapeChar: '\\'}, + expected: 'foo,"\\"quoted\\""' + }, + { + description: "test defeault escapeChar", + input: [{a: 'foo', b: '"quoted"'}], + config: {header: false}, + expected: 'foo,"""quoted"""' + }, + { + description: "Escape formulae", + input: [{ "Col1": "=danger", "Col2": "@danger", "Col3": "safe" }, { "Col1": "safe=safe", "Col2": "+danger", "Col3": "-danger, danger" }, { "Col1": "'+safe", "Col2": "'@safe", "Col3": "safe, safe" }], + config: { escapeFormulae: true }, + expected: 'Col1,Col2,Col3\r\n"\'=danger","\'@danger",safe\r\nsafe=safe,"\'+danger","\'-danger, danger"\r\n\'+safe,\'@safe,"safe, safe"' + }, + { + description: "Don't escape formulae by default", + input: [{ "Col1": "=danger", "Col2": "@danger", "Col3": "safe" }, { "Col1": "safe=safe", "Col2": "+danger", "Col3": "-danger, danger" }, { "Col1": "'+safe", "Col2": "'@safe", "Col3": "safe, safe" }], + expected: 'Col1,Col2,Col3\r\n=danger,@danger,safe\r\nsafe=safe,+danger,"-danger, danger"\r\n\'+safe,\'@safe,"safe, safe"' + }, + { + description: "Escape formulae with forced quotes", + input: [{ "Col1": "=danger", "Col2": "@danger", "Col3": "safe" }, { "Col1": "safe=safe", "Col2": "+danger", "Col3": "-danger, danger" }, { "Col1": "'+safe", "Col2": "'@safe", "Col3": "safe, safe" }], + config: { escapeFormulae: true, quotes: true }, + expected: '"Col1","Col2","Col3"\r\n"\'=danger","\'@danger","safe"\r\n"safe=safe","\'+danger","\'-danger, danger"\r\n"\'+safe","\'@safe","safe, safe"' + }, + { + description: "Escape formulae with single-quote quoteChar and escapeChar", + input: [{ "Col1": "=danger", "Col2": "@danger", "Col3": "safe" }, { "Col1": "safe=safe", "Col2": "+danger", "Col3": "-danger, danger" }, { "Col1": "'+safe", "Col2": "'@safe", "Col3": "safe, safe" }], + config: { escapeFormulae: true, quoteChar: "'", escapeChar: "'" }, + expected: 'Col1,Col2,Col3\r\n\'\'\'=danger\',\'\'\'@danger\',safe\r\nsafe=safe,\'\'\'+danger\',\'\'\'-danger, danger\'\r\n\'\'+safe,\'\'@safe,\'safe, safe\'' + }, + { + description: "Escape formulae with single-quote quoteChar and escapeChar and forced quotes", + input: [{ "Col1": "=danger", "Col2": "@danger", "Col3": "safe" }, { "Col1": "safe=safe", "Col2": "+danger", "Col3": "-danger, danger" }, { "Col1": "'+safe", "Col2": "'@safe", "Col3": "safe, safe" }], + config: { escapeFormulae: true, quotes: true, quoteChar: "'", escapeChar: "'" }, + expected: '\'Col1\',\'Col2\',\'Col3\'\r\n\'\'\'=danger\',\'\'\'@danger\',\'safe\'\r\n\'safe=safe\',\'\'\'+danger\',\'\'\'-danger, danger\'\r\n\'\'\'+safe\',\'\'\'@safe\',\'safe, safe\'' + }, + // new escapeFormulae values: + { + description: "Escape formulae with tab and carriage-return", + input: [{ "Col1": "\tdanger", "Col2": "\rdanger,", "Col3": "safe\t\r" }], + config: { escapeFormulae: true }, + expected: 'Col1,Col2,Col3\r\n"\'\tdanger","\'\rdanger,","safe\t\r"' + }, + { + description: "Escape formulae with tab and carriage-return, with forced quotes", + input: [{ "Col1": " danger", "Col2": "\rdanger,", "Col3": "safe\t\r" }], + config: { escapeFormulae: true, quotes: true }, + expected: '"Col1","Col2","Col3"\r\n"\'\tdanger","\'\rdanger,","safe\t\r"' + }, + { + description: "Escape formulae with tab and carriage-return, with single-quote quoteChar and escapeChar", + input: [{ "Col1": " danger", "Col2": "\rdanger,", "Col3": "safe, \t\r" }], + config: { escapeFormulae: true, quoteChar: "'", escapeChar: "'" }, + expected: 'Col1,Col2,Col3\r\n\'\'\'\tdanger\',\'\'\'\rdanger,\',\'safe, \t\r\'' + }, + { + description: "Escape formulae with tab and carriage-return, with single-quote quoteChar and escapeChar and forced quotes", + input: [{ "Col1": " danger", "Col2": "\rdanger,", "Col3": "safe, \t\r" }], + config: { escapeFormulae: true, quotes: true, quoteChar: "'", escapeChar: "'" }, + expected: '\'Col1\',\'Col2\',\'Col3\'\r\n\'\'\'\tdanger\',\'\'\'\rdanger,\',\'safe, \t\r\'' + }, +]; + +describe('Unparse Tests', function() { + function generateTest(test) { + (test.disabled ? it.skip : it)(test.description, function() { + var actual; + + try { + actual = Papa.unparse(test.input, test.config); + } catch (e) { + if (e instanceof Error) { + throw e; + } + actual = e; + } + + assert.strictEqual(actual, test.expected); + }); + } + + for (var i = 0; i < UNPARSE_TESTS.length; i++) { + generateTest(UNPARSE_TESTS[i]); + } +}); + + + +var CUSTOM_TESTS = [ + { + description: "Pause and resume works (Regression Test for Bug #636)", + disabled: !XHR_ENABLED, + timeout: 30000, + expected: [2001, [ + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Etiam a dolor vitae est vestibulum","84","DEF"], + ["Lorem ipsum dolor sit","42","ABC"], + ["Lorem ipsum dolor sit","42"] + ], 0], + run: function(callback) { + var stepped = 0; + var dataRows = []; + var errorCount = 0; + var output = []; + Papa.parse(BASE_PATH + "verylong-sample.csv", { + download: true, + step: function(results, parser) { + stepped++; + if (results) + { + parser.pause(); + parser.resume(); + if (results.data && stepped % 200 === 0) { + dataRows.push(results.data); + } + } + }, + complete: function() { + output.push(stepped); + output.push(dataRows); + output.push(errorCount); + callback(output); + } + }); + } + }, + { + description: "Pause and resume works for chunks with NetworkStreamer", + disabled: !XHR_ENABLED, + timeout: 30000, + expected: ["Etiam a dolor vitae est vestibulum", "84", "DEF"], + run: function(callback) { + var chunkNum = 0; + Papa.parse(BASE_PATH + "verylong-sample.csv", { + download: true, + chunkSize: 1000, + chunk: function(results, parser) { + chunkNum++; + parser.pause(); + + if (chunkNum === 2) { + callback(results.data[0]); + return; + } + + parser.resume(); + }, + complete: function() { + callback(new Error("Should have found matched row before parsing whole file")); + } + }); + } + }, + { + description: "Pause and resume works for chunks with FileStreamer", + disabled: !XHR_ENABLED, + timeout: 30000, + expected: ["Etiam a dolor vitae est vestibulum", "84", "DEF"], + run: function(callback) { + var chunkNum = 0; + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + Papa.parse(new File([xhr.responseText], './verylong-sample.csv'), { + chunkSize: 1000, + chunk: function(results, parser) { + chunkNum++; + parser.pause(); + + if (chunkNum === 2) { + callback(results.data[0]); + return; + } + + parser.resume(); + }, + complete: function() { + callback(new Error("Should have found matched row before parsing whole file")); + } + }); + }; + + xhr.open("GET", BASE_PATH + "verylong-sample.csv"); + try { + xhr.send(); + } catch (err) { + callback(err); + return; + } + } + }, + { + description: "Pause and resume works for chunks with StringStreamer", + disabled: !XHR_ENABLED, + timeout: 30000, + // Test also with string as byte size may be diferent + expected: ["Etiam a dolor vitae est vestibulum", "84", "DEF"], + run: function(callback) { + var chunkNum = 0; + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + Papa.parse(xhr.responseText, { + chunkSize: 1000, + chunk: function(results, parser) { + chunkNum++; + parser.pause(); + + if (chunkNum === 2) { + callback(results.data[0]); + return; + } + + parser.resume(); + }, + complete: function() { + callback(new Error("Should have found matched row before parsing whole file")); + } + }); + }; + + xhr.open("GET", BASE_PATH + "verylong-sample.csv"); + try { + xhr.send(); + } catch (err) { + callback(err); + return; + } + } + }, + { + description: "Complete is called with all results if neither step nor chunk is defined", + expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']], + disabled: !FILES_ENABLED, + run: function(callback) { + Papa.parse(new File(['A,b,c\nd,E,f\nG,h,i'], 'sample.csv'), { + chunkSize: 3, + complete: function(response) { + callback(response.data); + } + }); + } + }, + { + description: "Step is called for each row", + expected: 2, + run: function(callback) { + var callCount = 0; + Papa.parse('A,b,c\nd,E,f', { + step: function() { + callCount++; + }, + complete: function() { + callback(callCount); + } + }); + } + }, + { + description: "Data is correctly parsed with steps", + expected: [['A', 'b', 'c'], ['d', 'E', 'f']], + run: function(callback) { + var data = []; + Papa.parse('A,b,c\nd,E,f', { + step: function(results) { + data.push(results.data); + }, + complete: function() { + callback(data); + } + }); + } + }, + { + description: "Data is correctly parsed with steps (headers)", + expected: [{One: 'A', Two: 'b', Three: 'c'}, {One: 'd', Two: 'E', Three: 'f'}], + run: function(callback) { + var data = []; + Papa.parse('One,Two,Three\nA,b,c\nd,E,f', { + header: true, + step: function(results) { + data.push(results.data); + }, + complete: function() { + callback(data); + } + }); + } + }, + { + description: "Data is correctly parsed with steps and worker (headers)", + expected: [{One: 'A', Two: 'b', Three: 'c'}, {One: 'd', Two: 'E', Three: 'f'}], + run: function(callback) { + var data = []; + Papa.parse('One,Two,Three\nA,b,c\nd,E,f', { + header: true, + worker: true, + step: function(results) { + data.push(results.data); + }, + complete: function() { + callback(data); + } + }); + } + }, + { + description: "Data is correctly parsed with steps and worker", + expected: [['A', 'b', 'c'], ['d', 'E', 'f']], + run: function(callback) { + var data = []; + Papa.parse('A,b,c\nd,E,f', { + worker: true, + step: function(results) { + data.push(results.data); + }, + complete: function() { + callback(data); + } + }); + } + }, + { + description: "Data is correctly parsed with steps when skipping empty lines", + expected: [['A', 'b', 'c'], ['d', 'E', 'f']], + run: function(callback) { + var data = []; + Papa.parse('A,b,c\n\nd,E,f', { + skipEmptyLines: true, + step: function(results) { + data.push(results.data); + }, + complete: function() { + callback(data); + } + }); + } + }, + { + description: "Step is called with the contents of the row", + expected: ['A', 'b', 'c'], + run: function(callback) { + Papa.parse('A,b,c', { + step: function(response) { + callback(response.data); + } + }); + } + }, + { + description: "Step is called with the last cursor position", + expected: [6, 12, 17], + run: function(callback) { + var updates = []; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Step exposes cursor for downloads", + expected: [129, 287, 452, 595, 727, 865, 1031, 1209], + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Step exposes cursor for chunked downloads", + expected: [129, 287, 452, 595, 727, 865, 1031, 1209], + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Step exposes cursor for workers", + expected: [452, 452, 452, 865, 865, 865, 1209, 1209], + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + worker: true, + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Chunk is called for each chunk", + expected: [3, 3, 2], + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + chunk: function(response) { + updates.push(response.data.length); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Chunk is called with cursor position", + expected: [452, 865, 1209], + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + chunk: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Chunk functions can pause parsing", + expected: [ + [['A', 'b', 'c']] + ], + run: function(callback) { + var updates = []; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + chunkSize: 10, + chunk: function(response, handle) { + updates.push(response.data); + handle.pause(); + callback(updates); + }, + complete: function() { + callback(new Error('incorrect complete callback')); + } + }); + } + }, + { + description: "Chunk functions can resume parsing", + expected: [ + [['A', 'b', 'c']], + [['d', 'E', 'f'], ['G', 'h', 'i']] + ], + run: function(callback) { + var updates = []; + var handle = null; + var first = true; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + chunkSize: 10, + chunk: function(response, h) { + updates.push(response.data); + if (!first) return; + handle = h; + handle.pause(); + first = false; + }, + complete: function() { + callback(updates); + } + }); + setTimeout(function() { + handle.resume(); + }, 500); + } + }, + { + description: "Chunk functions can abort parsing", + expected: [ + [['A', 'b', 'c']] + ], + run: function(callback) { + var updates = []; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + chunkSize: 1, + chunk: function(response, handle) { + if (response.data.length) { + updates.push(response.data); + handle.abort(); + } + }, + complete: function(response) { + callback(updates); + } + }); + } + }, + { + description: "Step exposes indexes for files", + expected: [6, 12, 17], + disabled: !FILES_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(new File(['A,b,c\nd,E,f\nG,h,i'], 'sample.csv'), { + download: true, + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Step exposes indexes for chunked files", + expected: [6, 12, 17], + disabled: !FILES_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(new File(['A,b,c\nd,E,f\nG,h,i'], 'sample.csv'), { + chunkSize: 3, + step: function(response) { + updates.push(response.meta.cursor); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Quoted line breaks near chunk boundaries are handled", + expected: [['A', 'B', 'C'], ['X', 'Y\n1\n2\n3', 'Z']], + disabled: !FILES_ENABLED, + run: function(callback) { + var updates = []; + Papa.parse(new File(['A,B,C\nX,"Y\n1\n2\n3",Z'], 'sample.csv'), { + chunkSize: 3, + step: function(response) { + updates.push(response.data); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Step functions can abort parsing", + expected: [['A', 'b', 'c']], + run: function(callback) { + var updates = []; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + step: function(response, handle) { + updates.push(response.data); + handle.abort(); + callback(updates); + }, + chunkSize: 6 + }); + } + }, + { + description: "Complete is called after aborting", + expected: true, + run: function(callback) { + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + step: function(response, handle) { + handle.abort(); + }, + chunkSize: 6, + complete: function(response) { + callback(response.meta.aborted); + } + }); + } + }, + { + description: "Step functions can pause parsing", + expected: [['A', 'b', 'c']], + run: function(callback) { + var updates = []; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + step: function(response, handle) { + updates.push(response.data); + handle.pause(); + callback(updates); + }, + complete: function() { + callback('incorrect complete callback'); + } + }); + } + }, + { + description: "Step functions can resume parsing", + expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']], + run: function(callback) { + var updates = []; + var handle = null; + var first = true; + Papa.parse('A,b,c\nd,E,f\nG,h,i', { + step: function(response, h) { + updates.push(response.data); + if (!first) return; + handle = h; + handle.pause(); + first = false; + }, + complete: function() { + callback(updates); + } + }); + setTimeout(function() { + handle.resume(); + }, 500); + } + }, + { + description: "Step functions can abort workers", + expected: 1, + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = 0; + Papa.parse(BASE_PATH + "long-sample.csv", { + worker: true, + download: true, + chunkSize: 500, + step: function(response, handle) { + updates++; + handle.abort(); + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "beforeFirstChunk manipulates only first chunk", + expected: 7, + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = 0; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + beforeFirstChunk: function(chunk) { + return chunk.replace(/.*?\n/, ''); + }, + step: function(response) { + updates++; + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "First chunk not modified if beforeFirstChunk returns nothing", + expected: 8, + disabled: !XHR_ENABLED, + run: function(callback) { + var updates = 0; + Papa.parse(BASE_PATH + "long-sample.csv", { + download: true, + chunkSize: 500, + beforeFirstChunk: function(chunk) { + }, + step: function(response) { + updates++; + }, + complete: function() { + callback(updates); + } + }); + } + }, + { + description: "Should correctly guess custom delimiter when passed delimiters to guess.", + expected: "~", + run: function(callback) { + var results = Papa.parse('"A"~"B"~"C"~"D"', { + delimitersToGuess: ['~', '@', '%'] + }); + callback(results.meta.delimiter); + } + }, + { + description: "Should still correctly guess default delimiters when delimiters to guess are not given.", + expected: ",", + run: function(callback) { + var results = Papa.parse('"A","B","C","D"'); + callback(results.meta.delimiter); + } + }, + { + description: "Data is correctly parsed with chunks and duplicated headers", + expected: [{h0: 'a', h1: 'a'}, {h0: 'b', h1: 'b'}], + run: function(callback) { + var data = []; + Papa.parse('h0,h1\na,a\nb,b', { + header: true, + chunkSize: 10, + chunk: function(results) { + data.push(results.data[0]); + }, + complete: function() { + callback(data); + } + }); + } + }, +]; + +describe('Custom Tests', function() { + function generateTest(test) { + (test.disabled ? it.skip : it)(test.description, function(done) { + if(test.timeout) { + this.timeout(test.timeout); + } + test.run(function(actual) { + assert.deepEqual(actual, test.expected); + done(); + }); + }); + } + + for (var i = 0; i < CUSTOM_TESTS.length; i++) { + generateTest(CUSTOM_TESTS[i]); + } +}); + +(typeof window !== "undefined" ? describe : describe.skip)("Browser Tests", () => { + it("When parsing synchronously inside a web-worker not owned by PapaParse we should not invoke postMessage", async() => { + // Arrange + const papaParseScriptPath = new URL("../papaparse.js", window.document.baseURI).href; + + // Define our custom web-worker that loads PapaParse and executes a synchronous parse + const blob = new Blob([ + ` + importScripts('${papaParseScriptPath}'); + + self.addEventListener("message", function(event) { + if (event.data === "ExecuteParse") { + // Perform our synchronous parse, as requested + const results = Papa.parse('x\\ny\\n'); + postMessage({type: "ParseExecutedSuccessfully", results}); + } else { + // Otherwise, send whatever we received back. We shouldn't be hitting this (!) If we're reached + // this it means PapaParse thinks it is running inside a web-worker that it owns + postMessage(event.data); + } + }); + ` + ], {type: 'text/javascript'}); + + const blobURL = window.URL.createObjectURL(blob); + const webWorker = new Worker(blobURL); + + const receiveMessagePromise = new Promise((resolve, reject) => { + webWorker.addEventListener("message", (event) => { + if (event.data.type === "ParseExecutedSuccessfully") { + resolve(event.data); + } else { + const error = new Error(`Received unexpected message: ${JSON.stringify(event.data, null, 2)}`); + error.data = event.data; + reject(error); + } + }); + }); + + // Act + webWorker.postMessage("ExecuteParse"); + const webWorkerMessage = await receiveMessagePromise; + + // Assert + assert.equal("ParseExecutedSuccessfully", webWorkerMessage.type); + assert.equal(3, webWorkerMessage.results.data.length); + }); +}); diff --git a/node_modules/papaparse/tests/test.js b/node_modules/papaparse/tests/test.js new file mode 100644 index 0000000..2741831 --- /dev/null +++ b/node_modules/papaparse/tests/test.js @@ -0,0 +1,20 @@ +var connect = require('connect'); +var serveStatic = require('serve-static'); +var open = require('open'); +var path = require('path'); +var childProcess = require('child_process'); + +var server = connect().use(serveStatic(path.join(__dirname, '/..'))).listen(8071, function() { + if (process.argv.indexOf('--mocha-headless-chrome') !== -1) { + childProcess.spawn('node_modules/.bin/mocha-headless-chrome', ['-f', 'http://localhost:8071/tests/tests.html'], { + stdio: 'inherit' + }).on('exit', function(code) { + server.close(); + process.exit(code); // eslint-disable-line no-process-exit + }); + + } else { + open('http://localhost:8071/tests/tests.html'); + console.log('Serving tests...'); + } +}); diff --git a/node_modules/papaparse/tests/tests.html b/node_modules/papaparse/tests/tests.html new file mode 100644 index 0000000..a3ce51e --- /dev/null +++ b/node_modules/papaparse/tests/tests.html @@ -0,0 +1,22 @@ + + + Papa Parse Tests + + + + + + + + + + + +
+ + + + diff --git a/node_modules/papaparse/tests/utf-8-bom-sample.csv b/node_modules/papaparse/tests/utf-8-bom-sample.csv new file mode 100644 index 0000000..4f85ff0 --- /dev/null +++ b/node_modules/papaparse/tests/utf-8-bom-sample.csv @@ -0,0 +1,2 @@ +A,B,C +X,Y,Z diff --git a/node_modules/papaparse/tests/verylong-sample.csv b/node_modules/papaparse/tests/verylong-sample.csv new file mode 100644 index 0000000..14bc527 --- /dev/null +++ b/node_modules/papaparse/tests/verylong-sample.csv @@ -0,0 +1,2001 @@ +placeholder,meaning of life,TLD +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +"Lorem ipsum dolor sit",42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42 +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84 +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84 +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42,ABC +Lorem ipsum dolor sit,42,ABC +Etiam a dolor vitae est vestibulum,84,DEF +Lorem ipsum dolor sit,42 +Lorem ipsum dolor sit,42,ABC diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..19e029e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "inventur", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "papaparse": "^5.4.1" + } + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..96fc6cc --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "papaparse": "^5.4.1" + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..339bfad --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,111 @@ +use crate::Db; + +use rocket::response::Redirect; +use rocket::request; +use rocket::http::{Status, Cookie, CookieJar, SameSite}; +use inventur_db; +use rocket_oauth2::{OAuth2, TokenResponse}; +use reqwest::Client; +use rocket::serde::{Deserialize, json::Json}; + +pub struct RanderathIdentity; + + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +struct UnAuthUser { + preferred_username: String, + email: String, +} + +pub struct AuthUser { + pub uid: i32, + pub uname: String, + pub email: String, +} + +#[rocket::async_trait] +impl<'r> request::FromRequest<'r> for AuthUser { + type Error = (); + + async fn from_request(request: &'r request::Request<'_>) -> request::Outcome { + let cookies = request + .guard::<&CookieJar<'_>>() + .await + .expect("Cookie request failed..."); + let uid = cookies.get_private("uid"); + let uname = cookies.get_private("username"); + let email = cookies.get_private("email"); + if uid.is_some() && uname.is_some() && email.is_some() { + let uid = uid.unwrap().value().parse::().unwrap(); + let uname = uname.unwrap().value().to_string(); + let email = email.unwrap().value().to_string(); + return request::Outcome::Success(AuthUser { uid, uname, email }); + } + return request::Outcome::Forward(Status::Unauthorized); + } +} + +pub async fn login_or_register(conn: Db, access_token: &str) -> Option { + let ui = Client::new() + .get("https://ldap.randerath.eu/realms/master/protocol/openid-connect/userinfo") + .bearer_auth(access_token) + .send() + .await; + if ui.is_err() { + return None; + } + let ui = ui.unwrap().json::().await; + if ui.is_err() { + return None; + } + let ui = ui.unwrap(); + let user = conn.run(move |c| inventur_db::register_or_login(c, ui.preferred_username, ui.email)).await; + if user.is_none() { + return None; + } + Some(user.unwrap()) + +} + +#[catch(401)] +pub async fn redirect_to_login() -> Redirect { + Redirect::to(uri!(oauth_login())) +} + +#[get("/login")] +pub fn oauth_login(oauth2: OAuth2, cookies: &CookieJar<'_>) -> Redirect { + oauth2.get_redirect(cookies, &["openid"]).unwrap() +} + +#[get("/auth")] +pub async fn oauth_callback(conn: Db, token: TokenResponse, cookies: &CookieJar<'_>) -> Result { + 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) + .build() + ); + let user = login_or_register(conn, &at).await; + if user.is_none() { + return Err(Status::Forbidden); + } + let user = user.unwrap(); + + cookies.add_private( + Cookie::build(("username", user.uname)) + .same_site(SameSite::Lax) + .build()); + + cookies.add_private( + Cookie::build(("email", user.email)) + .same_site(SameSite::Lax) + .build()); + + cookies.add_private( + Cookie::build(("uid", user.uid.to_string())) + .same_site(SameSite::Lax) + .build()); + Ok(Redirect::to("/")) +} diff --git a/src/models.rs b/src/auth.rs~ similarity index 100% rename from src/models.rs rename to src/auth.rs~ diff --git a/src/main.rs b/src/main.rs index 12eedd5..ee60b6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,112 +1,74 @@ #[macro_use] extern crate rocket; -#[macro_use] extern crate rocket_db_pools; +mod auth; +mod table; +use auth::AuthUser; + +// TODO: filter (ajax, spinning circle), show options as in excel use rocket::fs::{FileServer, relative}; use rocket_dyn_templates::{Template, context}; -use rocket_db_pools::{Database, Connection}; -use rocket_db_pools::diesel::{QueryResult, MysqlPool, prelude::*}; -use rocket::form::Form; -/*use dotenvy::dotenv; -use std::env;*/ +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::{Toml, Env, Format}; -#[derive(Database)] #[database("inventur")] -struct Db(MysqlPool); +pub struct Db(diesel::MysqlConnection); -/*fn connect_db() -> MysqlConnection { - dotenv.ok(); - let database_url = env::var("DATABASE_URL").expect("Can't find database url"); - MysqlConnection::establish(&database_url).expect_or_else(|_| panic!("Couldn't connect to database {}.", database_url)); -}*/ -struct Owner { - id: i64, - email: String, - name: Option, -} - -#[derive(Queryable, Insertable)] -#[diesel(table_name = users)] -struct User { - id: i64, - email: String, -} - -diesel::table! { - users (id) { - id -> BigInt, - email -> Text, +#[get("/")] +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.unwrap(); + cols.push(tbl.column_names.clone()); + let mut rws : Vec> = Vec::new(); + for row in tbl.rows { + rws.push(row.cells); + } + rows.push(rws.clone()); } -} - -#[derive(Queryable, Insertable)] -#[diesel(table_name = jrtables)] -struct JRTable { - id: i64, - name: String, - owner_id: i64, -} - -diesel::table! { - jrtables (id) { - id -> BigInt, - name -> Text, - owner_id -> BigInt, - } -} - -#[get("/")] -fn table(tname: &str) -> Template { - let columns = ["name", "djhfae", "fsjhr"]; - let rows = [ - ["1", "first", "hasdjf", "753rgf"], - ["2", "second", "7438ued", "🚀"], - ["3", "third", "", ""] - ]; - Template::render("table", - context!{tname: tname, - columns: columns, - rows: rows, - } + Template::render("home", + context!{ + tids: tids, + tnames: tnames, + columns: cols, + rows: rows, + } ) } -#[derive(FromForm)] -struct New_table<'r> { - name: &'r str, - fields: Vec<&'r str>, - #[field(name = "done")] - complete: bool, -} - -#[post("/create", data="")] -fn create(data: Form>) { - //println!("{:?}", data); -} - - -#[derive(FromForm)] -struct Args <'r> { - value: &'r str, -} - -#[get("/table/?filter&&&")] -async fn filter(db:Connection, tname: &str, column: &str, mode: &str, args: Args<'_>) -> &'static str { - todo!() -} - -#[get("/")] -fn index(db: Connection) -> Template { - Template::render("test", context!{foo: 123,}) +#[get("/", rank=2)] +async fn login_home() -> Redirect { + Redirect::to(uri!(auth::oauth_login())) } #[launch] -fn rocket() -> _ { - rocket::build() +async fn rocket() -> _ { + dotenv().ok(); + + let cfg = Config::figment() + .merge(Env::prefixed("ROCKET_")); + + + rocket::custom(cfg) .attach(Template::fairing()) - .attach(Db::init()) - .mount("/", FileServer::from(relative!("static"))) - .mount("/", routes![index]) - .mount("/table", routes![create, table]) - //connect_db(); + .attach(Db::fairing()) + .attach(OAuth2::::fairing("oauth")) + .mount("/", routes![auth::oauth_login, auth::oauth_callback, home, login_home]) + .mount("/table", routes![table::table, table::table_sec, table::edit_tname, table::create, table::import_table]) + .mount("/entry", routes![table::new_entry]) + .register("/", catchers![auth::redirect_to_login]) + .mount("/static", FileServer::from(relative!("static"))) + + } diff --git a/src/schema.rs b/src/schema.rs index 98a5eb5..88b70c6 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,11 +1,10 @@ // @generated automatically by Diesel CLI. diesel::table! { - users (id) { + jrtables (id) { id -> Integer, - #[max_length = 64] - email -> Varchar, #[max_length = 255] - name -> Nullable, + name -> Varchar, + num_fields -> Integer, } } diff --git a/src/table.rs b/src/table.rs new file mode 100644 index 0000000..bcdba5a --- /dev/null +++ b/src/table.rs @@ -0,0 +1,180 @@ +use crate::auth; + +use auth::AuthUser; +use crate::Db; + +use rocket_dyn_templates::{Template, context}; +use rocket::form::Form; +use rocket::response::Redirect; +use inventur_db; +use rocket::serde::{Serialize, Deserialize, json::Json}; +use rocket::http::{Status, Cookie, CookieJar, SameSite}; + + + +#[derive(FromForm)] +struct NewEntry { + tblid: i32, + cells: Vec, +} + +#[derive(FromForm)] +struct EditTname { + tblid: i32, + new_name: String, +} + +#[derive(FromForm)] +struct NewTable { + name: String, + fields: Vec, +} + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +struct ImportTable { + name: String, + columns: Vec, + rows: Vec>, +} + +#[derive(Serialize)] +#[serde(crate = "rocket::serde")] +struct JRows { + rows: Vec>, +} + +pub async fn get_tids(conn: &Db, uid: i32) -> (Vec, Vec) { + let mut tids = conn.run(move |c| inventur_db::get_user_tblids(c, uid)).await; + let tnames; + if tids.is_none() { + tids = Some(Vec::new()); + tnames = Some(Vec::new()); + }else { + let tids = tids.clone().unwrap(); + tnames = conn.run(move |c| inventur_db::get_tblnames(c, tids)).await; + } + (tids.unwrap(), tnames.unwrap()) +} + +#[get("/", rank=2)] +pub async fn table_sec(conn: Db, tid: i32, user: AuthUser) -> Redirect { + let nus : Option = None; + let nu8 : Option = None; + let nvi32 : Option> = None; + let ns : Option = None; + Redirect::to(uri!("/table", table(tid, nu8, nus, nvi32, ns))) +} + +#[get("/?&&&")] +pub async fn table(conn: Db, tid: i32, sort_dir: Option, sort_field: Option, search_fields: Option>, search_value: Option, user: AuthUser) -> Option