feature/partials #59

Manually merged
Sceptique merged 6 commits from feature/partials into master 2021-06-24 22:12:23 +02:00
12 changed files with 218 additions and 63 deletions

View File

@ -2,4 +2,4 @@ LIFEPEX_DB=sqlite://sqlite.db
LIFEPEX_BIND=127.0.0.1 LIFEPEX_BIND=127.0.0.1
LIFEPEX_BASE_URL= LIFEPEX_BASE_URL=
LIFEPEX_SECRET= LIFEPEX_SECRET=
LIFEPEX_ENV= PORT=

View File

@ -81,4 +81,17 @@ xdg-open ./public/doc/index.html
### Testing ### Testing
If you want to run the test, simply type `rake test` (you will need the startup env variable to be set first). Generate first a specific configuration file
```
cp .env.local .env.test.local
editor .env.test.local # don't forget to set a new database !!!
```
Then init the database
```
LIFEPEX_ENV=test ./init/database.rb
```
Then if you want to run the test, simply type `rake test` (you will need the startup env variable to be set first).

34
public/js/ajax.js Normal file
View File

@ -0,0 +1,34 @@
async function ajax({
method = "GET",
url = ".",
body = undefined,
headers = {},
on_success = () => {},
on_failure = () => {},
}) {
const xhttp = new XMLHttpRequest();
const return_on_sent = new Promise((resolve, reject) => {
try {
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4) {
const response_status = String(xhttp.status);
if (/2\d\d/.test(response_status)) {
resolve(on_success(xhttp.responseText, xhttp));
} else {
resolve(on_failure(xhttp.responseText, xhttp));
}
}
};
} catch (err) {
reject(err);
}
});
xhttp.open(method, url, true);
Object.keys(headers).forEach((header_key) => {
xhttp.setRequestHeader(header_key, headers[header_key]);
});
xhttp.send(body);
return return_on_sent;
}

11
public/js/error.js Normal file
View File

@ -0,0 +1,11 @@
function flashError(message) {
const new_flash_error = document.createElement('p');
new_flash_error.classList.add("alert");
new_flash_error.classList.add("alert-danger");
new_flash_error.classList.add("alert-dismissible");
new_flash_error.classList.add("fade");
new_flash_error.classList.add("show");
new_flash_error.setAttribute("role", "alert");
new_flash_error.innerHTML = `${message} <button class=\"btn-close\" type=\"button\" data-bs-dismiss=\"alert\" aria-label=\"Close\" />`;
const flash = document.querySelectorAll('.flash')[0].appendChild(new_flash_error);
}

View File

@ -17,10 +17,70 @@ function toggle(node) {
} }
} }
document.addEventListener("DOMContentLoaded", (_event) => { Array.toObject = function (arr) {
const togglers = document.querySelectorAll('.pex-editor-toggler'); return arr.reduce((base, current) => {
base[current[0]] = current[1];
return base;
}, {});
};
togglers.forEach((t) => { function __map__(cb = (e) => e) {
const arr = [];
for (i = 0; i < this.length; i++) {
arr.push(cb(this[i]));
}
return arr;
}
HTMLCollection.prototype.map = __map__;
NodeList.prototype.map = __map__;
NamedNodeMap.prototype.map = __map__;
HTMLInputElement.prototype.attributesObject = function () {
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
};
HTMLFormElement.prototype.attributesObject = function () {
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
};
function parseCookie(str) {
return str
.split(";")
.map((v) => v.split("="))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {});
}
function userpexValidation(event) {
event.preventDefault();
const attributes = event.target.attributesObject();
const fields = event.target.childNodes.map().filter(e => e.nodeName == 'INPUT');
const fields_attributes = Array.toObject(fields.map(attr => [attr.name, attr.value]));
ajax({
method: fields_attributes["type"] == "-" ? "DELETE" : "POST",
url: `/api/user-pex/v1/pexs/${fields_attributes["id"]}/validation`,
body: JSON.stringify({ date: parseCookie(document.cookie).date, force_count_total: true }),
headers: { Accept: "application/json", "Content-Type": "application/json" },
on_success: (body) => {
const json_body = JSON.parse(body);
const text_target = event.target.parentNode.parentElement.childNodes[3];
text_target.textContent = String(json_body.count_total);
const should_hide_decrease_button = json_body.count_total == 0;
event.target.parentNode.parentNode.childNodes[2].childNodes[0].hidden = should_hide_decrease_button;
},
on_failure: (body, req) => {
flashError("Error JS#0001 while validating...");
},
});
}
document.addEventListener("DOMContentLoaded", (_event) => {
const editor_togglers = document.querySelectorAll('.pex-editor-toggler');
editor_togglers.forEach((t) => {
const name = t.attributes.name.value; const name = t.attributes.name.value;
const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`); const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
hide(pex_editor); hide(pex_editor);
@ -29,4 +89,9 @@ document.addEventListener("DOMContentLoaded", (_event) => {
toggle(pex_editor); toggle(pex_editor);
}); });
}); });
const userpexvalidation0 = document.querySelectorAll('.userpexvalidationvalue').map().filter(tag => tag.textContent == "0");
userpexvalidation0.forEach((tag) => {
tag.parentNode.parentNode.childNodes[2].childNodes[0].hidden = true;
});
}); });

View File

@ -15,16 +15,15 @@ function setupChart(...cumuls) {
}); });
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', async function () {
const xhttp = new XMLHttpRequest(); ajax({
xhttp.onreadystatechange = () => { method: "GET",
if (xhttp.readyState == 4 && xhttp.status == 200) { url: "/api/pex/v1/recap",
const json_output = JSON.parse(xhttp.responseText); body: null,
headers: { Accept: "application/json" },
on_success: (body) => {
const json_output = JSON.parse(body);
setupChart(...json_output.pex_tables); setupChart(...json_output.pex_tables);
} },
}; });
xhttp.open("GET", "/api/pex/v1/recap", true);
xhttp.setRequestHeader("Accept", "application/json");
xhttp.send();
}); });

View File

@ -18,8 +18,12 @@ require "active_support"
require "active_support/core_ext" require "active_support/core_ext"
require "pry" # debug require "pry" # debug
module LifePex
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
end
require "dotenv" require "dotenv"
Dotenv.load(".env.local", ".env") Dotenv.load(".env.#{LifePex::APP_ENV}.local", ".env.local", ".env")
require_relative "./utils/url.rb" require_relative "./utils/url.rb"
require_relative "./utils/boot_framework" require_relative "./utils/boot_framework"
@ -42,7 +46,6 @@ module LifePex
SECRET = ENV["LIFEPEX_SECRET"] SECRET = ENV["LIFEPEX_SECRET"]
CODE_VERSION = DB[:meta].first[:code_version] CODE_VERSION = DB[:meta].first[:code_version]
CODE_DATE = DB[:meta].first[:code_date] CODE_DATE = DB[:meta].first[:code_date]
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
include LifePex::Utils::Url include LifePex::Utils::Url
@ -71,7 +74,8 @@ class LifePex::App < Sinatra::Base
set :session_secret, LifePex::SECRET set :session_secret, LifePex::SECRET
enable :sessions enable :sessions
use Rack::Csrf, skip: ["*:/api*"] if ENV["LIFEPEX_ENV"] != "test" # use Rack::Csrf, skip: ["POST:/api/*"], :raise => (LifePex::APP_ENV != "production") if LifePex::APP_ENV != "test"
set :protection, :except => :json_csrf if LifePex::APP_ENV != "test"
LifePex::Systems.constants LifePex::Systems.constants
.filter { |system| system.to_s =~ /System$/ } .filter { |system| system.to_s =~ /System$/ }

View File

@ -4,11 +4,12 @@ module LifePex::Systems::ApiResponse
any.to_json any.to_json
end end
def api_response_entity(message = nil, entity_type = nil, entity = nil) def api_response_entity(message = nil, entity_type = nil, entity = nil, **more)
api_response({ api_response({
"message" => message, "message" => message,
"entity_type" => entity_type, "entity_type" => entity_type,
entity_type => entity, entity_type => entity,
**more,
}.compact) }.compact)
end end
@ -16,6 +17,6 @@ module LifePex::Systems::ApiResponse
halt(status, { halt(status, {
message => message, message => message,
**more, **more,
}) }.to_json)
end end
end end

View File

@ -26,12 +26,12 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
end end
get "/today", auth: [] do get "/today", auth: [] do
cookies["date"] = "today" cookies.set "date", { value: "today", httponly: false }
redirect "/" redirect "/"
end end
get "/yesterday", auth: [] do get "/yesterday", auth: [] do
cookies["date"] = "yesterday" cookies.set "date", { value: "yesterday", httponly: false }
redirect "/" redirect "/"
end end

View File

@ -4,17 +4,33 @@ require_relative "./api_response"
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
include JSON::API include JSON::API
include LifePex::Systems::ApiResponse include LifePex::Systems::ApiResponse
set :protection, :except => [:frame_options, :json_csrf]
def date_input_convertor(date)
if date && Date.respond_to?(date)
Date.send date
elsif date == "yesterday"
Date.today - 1
else
begin
Date.parse date
rescue => _
Date.today
end
end
end
extend DocMyRoutes::Annotatable # included by PexSystem extend DocMyRoutes::Annotatable # included by PexSystem
register Sinatra::Namespace # included by PexSystem register Sinatra::Namespace # included by PexSystem
namespace '/api/user-pex/v1' do namespace "/api/user-pex/v1" do
namespace "/pexs" do
namespace '/pexs' do summary "Get the amount of user_pex for a given day"
produces "application/json"
summary 'Get the amount of user_pex for a given day'
produces 'application/json'
status_codes [200] status_codes [200]
get "/:pex_id/amount/by-date/:date", auth: [], provides: 'json' do parameter :id, required: true, type: "integer", in: "path"
parameter :date, required: true, type: "string", in: "path"
get "/:pex_id/amount/by-date/:date", auth: [], provides: "json" do
pex_id = params["pex_id"] pex_id = params["pex_id"]
date = params["date"] date = params["date"]
count = LifePex::UserPex.where( count = LifePex::UserPex.where(
@ -22,47 +38,66 @@ class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
pex_id: pex_id, pex_id: pex_id,
created_at: date, created_at: date,
).count ).count
api_response({ count: count, entity_type: 'user_pex' }) api_response({ count: count, entity_type: "user_pex" })
end end
summary 'Create a new user_pex for a given day' summary "Create a new user_pex for a given day"
produces 'application/json' produces "application/json"
status_codes [200] status_codes [200]
post "/:pex_id/validation", auth: [], provides: 'json' do parameter :id, required: true, type: "integer", in: "path"
parameter :date, required: false, type: "string", in: "body"
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
post "/:pex_id/validation", auth: [], provides: "json" do
pex_id = params["pex_id"] pex_id = params["pex_id"]
date = json_params["date"] || Date.today.to_s date = date_input_convertor(json_params["date"])
api_error(403, "You may not have created or access to this pex id") if LifePex::Pex.where(id: pex_id).select(:user_id).first&.user_id != current_user_id
user_pex = LifePex::UserPex.new( user_pex = LifePex::UserPex.new(
user_id: current_user_id, user_id: current_user_id,
pex_id: pex_id, pex_id: pex_id,
created_at: date, created_at: date,
).save ).save
count_total = LifePex::UserPex.where(
user_id: current_user_id,
pex_id: pex_id,
created_at: date,
).count if json_params["force_count_total"] == true
api_response_entity( api_response_entity(
"Successfuly added one user_pex", "Successfuly added one user_pex",
"user_pex", "user_pex",
user_pex, user_pex,
count_total: count_total,
) )
end end
summary 'Remove an existing user_pex for a given day' summary "Remove an existing user_pex for a given day"
produces 'application/json' produces "application/json"
status_codes [200] status_codes [200]
delete "/:pex_id/validation", auth: [], provides: 'json' do parameter :id, required: true, type: "integer", in: "path"
parameter :date, required: false, type: "string", in: "body"
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
delete "/:pex_id/validation", auth: [], provides: "json" do
pex_id = params["pex_id"] pex_id = params["pex_id"]
date = json_params["date"] || Date.today.to_s date = date_input_convertor(json_params["date"])
user_pex = LifePex::UserPex.find( user_pex = LifePex::UserPex.where(
user_id: current_user_id, user_id: current_user_id,
pex_id: pex_id, pex_id: pex_id,
created_at: date, created_at: date,
) ).select(:id).first
if user_pex if user_pex
user_pex.destroy user_pex.destroy
count_total = LifePex::UserPex.where(
user_id: current_user_id,
pex_id: pex_id,
created_at: date,
).count if json_params["force_count_total"] == true
api_response({ api_response({
message: "Successfuly destroyed 1 user_pex", message: "Successfuly destroyed 1 user_pex",
entity_type: "user_pex", entity_type: "user_pex",
count: 1, count_destroyed: 1,
count_total: count_total,
}) })
else else
api_error(400, "Nothing to destroy") api_error(404, "Nothing to destroy")
end end
end end

View File

@ -47,17 +47,17 @@
| u | u
td.col-8=pex_by_model[:name] td.col-8=pex_by_model[:name]
td.col-1.center td.col-1.center
- if pex_by_model[:user_pexs][:amount] > 0 form.userpexvalidation.userpexvalidationdecrease method="POST" action="/" onsubmit="return userpexValidation(event)"
form method="POST" action="/" == csrf_tag
== csrf_tag input type="hidden" name="id" value=pex_by_model[:id]
input type="hidden" name="id" value=pex_by_model[:id] input type="hidden" name="type" value="-"
input type="hidden" name="type" value="-" button.btn.force-1-col type="submit" style="display: block;"
button.btn.force-1-col type="submit" style="display: block;" | -
| -
td.col-1.center td.col-1.center
=pex_by_model[:user_pexs][:amount] .userpexvalidation.userpexvalidationvalue
=pex_by_model[:user_pexs][:amount]
td.col-1.center td.col-1.center
form method="POST" action="/" form.userpexvalidation.userpexvalidationincrease method="POST" action="/" onsubmit="return userpexValidation(event)"
== csrf_tag == csrf_tag
input type="hidden" name="id" value=pex_by_model[:id] input type="hidden" name="id" value=pex_by_model[:id]
input type="hidden" name="type" value="+" input type="hidden" name="type" value="+"

View File

@ -2,7 +2,8 @@ doctype html
html lang="en" html lang="en"
head head
/! Required meta tags /! Required meta tags
title Life Pex title
| Life Pex
meta charset="utf-8" / meta charset="utf-8" /
meta content="width=device-width, initial-scale=1" name="viewport" / meta content="width=device-width, initial-scale=1" name="viewport" /
/! Bootstrap CSS /! Bootstrap CSS
@ -46,16 +47,6 @@ html lang="en"
li.nav-item li.nav-item
a.btn.btn-lg.btn-dark href="/logout" Logout a.btn.btn-lg.btn-dark href="/logout" Logout
/ li.nav-item.dropdown
/ a.btn.btn-lg.btn-dark.nav-link.dropdown-toggle#nav-drop-more href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"
/ | More
/ ul.dropdown-menu aria-labelledby="nav-drop-more"
/ li
/ a.btn.btn-lg.btn-dark href="/password" Change password
/ li
/ a.btn.btn-lg.btn-dark href="/about" About lifepex
/ li
/ a.btn.btn-lg.btn-dark href="/logout" Logout
- else - else
li.nav-item li.nav-item
a.btn.btn-lg.btn-dark href="/login" Login a.btn.btn-lg.btn-dark href="/login" Login
@ -65,7 +56,7 @@ html lang="en"
a.btn.btn-lg.btn-dark href="/about" About lifepex a.btn.btn-lg.btn-dark href="/about" About lifepex
.flash #flash.flash
- if defined? flash - if defined? flash
- flash.each do |flash_name, flash_message| - flash.each do |flash_name, flash_message|
.alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}" .alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}"
@ -81,3 +72,5 @@ html lang="en"
/ script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous" / script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"
script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous" script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous" script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"
script src="/js/error.js"
script src="/js/ajax.js"