feature/partials #59
|
@ -2,4 +2,4 @@ LIFEPEX_DB=sqlite://sqlite.db
|
|||
LIFEPEX_BIND=127.0.0.1
|
||||
LIFEPEX_BASE_URL=
|
||||
LIFEPEX_SECRET=
|
||||
LIFEPEX_ENV=
|
||||
PORT=
|
15
README.md
15
README.md
|
@ -81,4 +81,17 @@ xdg-open ./public/doc/index.html
|
|||
|
||||
### 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
34
public/js/ajax.js
Normal 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
11
public/js/error.js
Normal 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);
|
||||
}
|
|
@ -17,10 +17,70 @@ function toggle(node) {
|
|||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (_event) => {
|
||||
const togglers = document.querySelectorAll('.pex-editor-toggler');
|
||||
Array.toObject = function (arr) {
|
||||
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 pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
|
||||
hide(pex_editor);
|
||||
|
@ -29,4 +89,9 @@ document.addEventListener("DOMContentLoaded", (_event) => {
|
|||
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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,16 +15,15 @@ function setupChart(...cumuls) {
|
|||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = () => {
|
||||
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||
const json_output = JSON.parse(xhttp.responseText);
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
ajax({
|
||||
method: "GET",
|
||||
url: "/api/pex/v1/recap",
|
||||
body: null,
|
||||
headers: { Accept: "application/json" },
|
||||
on_success: (body) => {
|
||||
const json_output = JSON.parse(body);
|
||||
setupChart(...json_output.pex_tables);
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open("GET", "/api/pex/v1/recap", true);
|
||||
xhttp.setRequestHeader("Accept", "application/json");
|
||||
xhttp.send();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
10
src/app.rb
10
src/app.rb
|
@ -18,8 +18,12 @@ require "active_support"
|
|||
require "active_support/core_ext"
|
||||
require "pry" # debug
|
||||
|
||||
module LifePex
|
||||
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
|
||||
end
|
||||
|
||||
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/boot_framework"
|
||||
|
@ -42,7 +46,6 @@ module LifePex
|
|||
SECRET = ENV["LIFEPEX_SECRET"]
|
||||
CODE_VERSION = DB[:meta].first[:code_version]
|
||||
CODE_DATE = DB[:meta].first[:code_date]
|
||||
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
|
||||
|
||||
include LifePex::Utils::Url
|
||||
|
||||
|
@ -71,7 +74,8 @@ class LifePex::App < Sinatra::Base
|
|||
set :session_secret, LifePex::SECRET
|
||||
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
|
||||
.filter { |system| system.to_s =~ /System$/ }
|
||||
|
|
|
@ -4,11 +4,12 @@ module LifePex::Systems::ApiResponse
|
|||
any.to_json
|
||||
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({
|
||||
"message" => message,
|
||||
"entity_type" => entity_type,
|
||||
entity_type => entity,
|
||||
**more,
|
||||
}.compact)
|
||||
end
|
||||
|
||||
|
@ -16,6 +17,6 @@ module LifePex::Systems::ApiResponse
|
|||
halt(status, {
|
||||
message => message,
|
||||
**more,
|
||||
})
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,12 +26,12 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
|||
end
|
||||
|
||||
get "/today", auth: [] do
|
||||
cookies["date"] = "today"
|
||||
cookies.set "date", { value: "today", httponly: false }
|
||||
redirect "/"
|
||||
end
|
||||
|
||||
get "/yesterday", auth: [] do
|
||||
cookies["date"] = "yesterday"
|
||||
cookies.set "date", { value: "yesterday", httponly: false }
|
||||
redirect "/"
|
||||
end
|
||||
|
||||
|
|
|
@ -4,17 +4,33 @@ require_relative "./api_response"
|
|||
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
||||
include JSON::API
|
||||
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
|
||||
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]
|
||||
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"]
|
||||
date = params["date"]
|
||||
count = LifePex::UserPex.where(
|
||||
|
@ -22,47 +38,66 @@ class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
|||
pex_id: pex_id,
|
||||
created_at: date,
|
||||
).count
|
||||
api_response({ count: count, entity_type: 'user_pex' })
|
||||
api_response({ count: count, entity_type: "user_pex" })
|
||||
end
|
||||
|
||||
summary 'Create a new user_pex for a given day'
|
||||
produces 'application/json'
|
||||
summary "Create a new user_pex for a given day"
|
||||
produces "application/json"
|
||||
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"]
|
||||
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_id: current_user_id,
|
||||
pex_id: pex_id,
|
||||
created_at: date,
|
||||
).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(
|
||||
"Successfuly added one user_pex",
|
||||
"user_pex",
|
||||
user_pex,
|
||||
count_total: count_total,
|
||||
)
|
||||
end
|
||||
|
||||
summary 'Remove an existing user_pex for a given day'
|
||||
produces 'application/json'
|
||||
summary "Remove an existing user_pex for a given day"
|
||||
produces "application/json"
|
||||
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"]
|
||||
date = json_params["date"] || Date.today.to_s
|
||||
user_pex = LifePex::UserPex.find(
|
||||
date = date_input_convertor(json_params["date"])
|
||||
user_pex = LifePex::UserPex.where(
|
||||
user_id: current_user_id,
|
||||
pex_id: pex_id,
|
||||
created_at: date,
|
||||
)
|
||||
).select(:id).first
|
||||
if user_pex
|
||||
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({
|
||||
message: "Successfuly destroyed 1 user_pex",
|
||||
entity_type: "user_pex",
|
||||
count: 1,
|
||||
count_destroyed: 1,
|
||||
count_total: count_total,
|
||||
})
|
||||
else
|
||||
api_error(400, "Nothing to destroy")
|
||||
api_error(404, "Nothing to destroy")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -47,17 +47,17 @@
|
|||
| u
|
||||
td.col-8=pex_by_model[:name]
|
||||
td.col-1.center
|
||||
- if pex_by_model[:user_pexs][:amount] > 0
|
||||
form method="POST" action="/"
|
||||
== csrf_tag
|
||||
input type="hidden" name="id" value=pex_by_model[:id]
|
||||
input type="hidden" name="type" value="-"
|
||||
button.btn.force-1-col type="submit" style="display: block;"
|
||||
| -
|
||||
form.userpexvalidation.userpexvalidationdecrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||
== csrf_tag
|
||||
input type="hidden" name="id" value=pex_by_model[:id]
|
||||
input type="hidden" name="type" value="-"
|
||||
button.btn.force-1-col type="submit" style="display: block;"
|
||||
| -
|
||||
td.col-1.center
|
||||
=pex_by_model[:user_pexs][:amount]
|
||||
.userpexvalidation.userpexvalidationvalue
|
||||
=pex_by_model[:user_pexs][:amount]
|
||||
td.col-1.center
|
||||
form method="POST" action="/"
|
||||
form.userpexvalidation.userpexvalidationincrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||
== csrf_tag
|
||||
input type="hidden" name="id" value=pex_by_model[:id]
|
||||
input type="hidden" name="type" value="+"
|
||||
|
|
|
@ -2,7 +2,8 @@ doctype html
|
|||
html lang="en"
|
||||
head
|
||||
/! Required meta tags
|
||||
title Life Pex
|
||||
title
|
||||
| Life Pex
|
||||
meta charset="utf-8" /
|
||||
meta content="width=device-width, initial-scale=1" name="viewport" /
|
||||
/! Bootstrap CSS
|
||||
|
@ -46,16 +47,6 @@ html lang="en"
|
|||
li.nav-item
|
||||
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
|
||||
li.nav-item
|
||||
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
|
||||
|
||||
|
||||
.flash
|
||||
#flash.flash
|
||||
- if defined? flash
|
||||
- flash.each do |flash_name, flash_message|
|
||||
.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://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="/js/error.js"
|
||||
script src="/js/ajax.js"
|
||||
|
|
Loading…
Reference in New Issue
Block a user