Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
35b69ef33a | |||
46d14c2917 | |||
3d607ade56 | |||
08a2729ce6 | |||
d4de167df2 | |||
29e78b7f52 | |||
5f8afb008e | |||
b9bc73b3b8 | |||
8efe5d3ab3 | |||
db9c5cd7a4 | |||
76da1fd1a8 | |||
1f44afb893 | |||
0485f8131d | |||
|
ac29e5bb41 | ||
|
690d41d33f | ||
|
fef976ed5c | ||
c2b5dd96b2 | |||
5a24cdb6b7 | |||
ab708456ed | |||
f2ad52586f | |||
d1fae9bf63 | |||
fb02993fa3 | |||
b36249ae76 | |||
2559c8c7a7 | |||
fd23535924 | |||
0e26243315 | |||
9315c422a0 | |||
08c895f3a5 | |||
52a8d4b5fe | |||
71e9d7c745 | |||
e3377979ed | |||
6f0877264f | |||
b9bd6674f6 | |||
51c513e358 | |||
fd71187d53 | |||
aa479693a3 | |||
3523add226 | |||
f6a951f704 | |||
531756e3b6 | |||
7eb4b680a5 | |||
3d7028e291 | |||
0f25ed78e0 | |||
612cee76a0 | |||
a9652a63a9 | |||
46d2d3c8d1 | |||
f350c56e9c | |||
1b4fac518e | |||
9cef2c7a59 | |||
9972bff604 | |||
6572a40a85 | |||
7c6d0307a0 | |||
d8dcd0f1e5 | |||
0a89fe1c36 | |||
a2eeff353a | |||
ff1853e01c | |||
548172b55f | |||
15a16e0110 | |||
ef61f05607 | |||
93a7c2dcf9 | |||
575e8450a4 | |||
a41c22b211 | |||
0dfd95761a | |||
ac32362255 | |||
237835eb08 | |||
e7f7d6088f | |||
2d46801e65 | |||
50d56a1803 | |||
e7e3d0ec6c | |||
a6ed3bf9d8 | |||
05e327e47b | |||
be055ad965 | |||
1f44a23e4c | |||
e9c6837cb5 | |||
9e0fb6e946 | |||
84e87bd6f8 | |||
73ef6f4bfa | |||
261b967b8c | |||
2cfaf82cb5 | |||
364ed2f9f2 | |||
6713aefeda | |||
595e4189d3 | |||
8cc5dfc160 | |||
11b9ec2ce5 | |||
0f010caa68 | |||
b341ef4469 | |||
c9d9ec9a01 | |||
6434587f89 | |||
cc5f37b61b | |||
205b0cea8e | |||
b34349855e | |||
1010dbdac8 | |||
df505a7c8c | |||
c217fa0f86 | |||
ebabab2586 | |||
ed2573e657 | |||
99a4c368e9 |
13
.drone.yml
Normal file
13
.drone.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: ruby:3.1
|
||||||
|
environment:
|
||||||
|
LIFEPEX_DB: "sqlite://test.db"
|
||||||
|
LIFEPEX_ENV: "test"
|
||||||
|
commands:
|
||||||
|
- bundle install --jobs=1 --retry=1
|
||||||
|
- rake db:migrate
|
||||||
|
- rake test
|
5
.env.sample
Normal file
5
.env.sample
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
LIFEPEX_DB=sqlite://sqlite.db
|
||||||
|
LIFEPEX_BIND=127.0.0.1
|
||||||
|
LIFEPEX_BASE_URL=
|
||||||
|
LIFEPEX_SECRET=
|
||||||
|
PORT=
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
*.db
|
*.db
|
||||||
|
coverage
|
||||||
|
.env.*local
|
||||||
|
|
42
Gemfile
42
Gemfile
|
@ -2,17 +2,35 @@
|
||||||
|
|
||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem "slim", "~> 4.1"
|
# web
|
||||||
gem "pry", "~> 0.14.1"
|
gem "puma", "~> 5"
|
||||||
gem "sequel", "~> 5.43"
|
gem "sinatra", "~> 2"
|
||||||
gem "bcrypt", "~> 3.1"
|
gem "sinatra-contrib", "~> 2"
|
||||||
gem "sinatra", "~> 2.1"
|
gem "slim", "~> 4"
|
||||||
gem "sinatra-contrib", "~> 2.1"
|
|
||||||
gem "jwt", "~> 2.2"
|
|
||||||
|
|
||||||
gem "pg", "~> 1.2"
|
# database
|
||||||
|
gem "sequel", "~> 5"
|
||||||
|
# you # comment the drivers you don't want
|
||||||
|
gem "sqlite3", "~> 1"
|
||||||
|
# gem "pg", "~> 1.2"
|
||||||
|
|
||||||
gem "colorize", "~> 0.8.1"
|
# security
|
||||||
gem "doc_my_routes",
|
gem "jwt", "~> 2"
|
||||||
:git => "https://github.com/Nephos/doc_my_routes.git",
|
gem "bcrypt", "~> 3"
|
||||||
branch: "master"
|
gem "rack_csrf", "~> 2"
|
||||||
|
|
||||||
|
# api tools
|
||||||
|
gem "doc_my_routes"
|
||||||
|
|
||||||
|
# debug and helpers
|
||||||
|
gem "colorize", "~> 0.8"
|
||||||
|
gem "activesupport", "~> 6"
|
||||||
|
|
||||||
|
# tests
|
||||||
|
group :test do
|
||||||
|
gem "pry"
|
||||||
|
gem "rack-test", "~> 1", require: false
|
||||||
|
# gem "simplecov", "~> 0.21.2", require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
gem "dotenv", "~> 2"
|
||||||
|
|
86
Gemfile.lock
86
Gemfile.lock
|
@ -1,61 +1,81 @@
|
||||||
GIT
|
|
||||||
remote: https://github.com/Nephos/doc_my_routes.git
|
|
||||||
revision: 3325f74615ac254ebf897872593a99f8dfb8a70e
|
|
||||||
branch: master
|
|
||||||
specs:
|
|
||||||
doc_my_routes (0.13.0)
|
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
bcrypt (3.1.16)
|
activesupport (6.1.6.1)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
tzinfo (~> 2.0)
|
||||||
|
zeitwerk (~> 2.3)
|
||||||
|
bcrypt (3.1.18)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
colorize (0.8.1)
|
colorize (0.8.1)
|
||||||
jwt (2.2.3)
|
concurrent-ruby (1.1.10)
|
||||||
|
doc_my_routes (0.13.0)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
i18n (1.12.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
jwt (2.4.1)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
|
minitest (5.16.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
mustermann (1.1.1)
|
mustermann (2.0.2)
|
||||||
ruby2_keywords (~> 0.0.1)
|
ruby2_keywords (~> 0.0.1)
|
||||||
pg (1.2.3)
|
nio4r (2.5.8)
|
||||||
pry (0.14.1)
|
pry (0.14.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
rack (2.2.3)
|
puma (5.6.4)
|
||||||
rack-protection (2.1.0)
|
nio4r (~> 2.0)
|
||||||
|
rack (2.2.4)
|
||||||
|
rack-protection (2.2.2)
|
||||||
rack
|
rack
|
||||||
ruby2_keywords (0.0.4)
|
rack-test (1.1.0)
|
||||||
sequel (5.43.0)
|
rack (>= 1.0, < 3)
|
||||||
sinatra (2.1.0)
|
rack_csrf (2.6.0)
|
||||||
mustermann (~> 1.0)
|
rack (>= 1.1.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
sequel (5.58.0)
|
||||||
|
sinatra (2.2.2)
|
||||||
|
mustermann (~> 2.0)
|
||||||
rack (~> 2.2)
|
rack (~> 2.2)
|
||||||
rack-protection (= 2.1.0)
|
rack-protection (= 2.2.2)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
sinatra-contrib (2.1.0)
|
sinatra-contrib (2.2.2)
|
||||||
multi_json
|
multi_json
|
||||||
mustermann (~> 1.0)
|
mustermann (~> 2.0)
|
||||||
rack-protection (= 2.1.0)
|
rack-protection (= 2.2.2)
|
||||||
sinatra (= 2.1.0)
|
sinatra (= 2.2.2)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
slim (4.1.0)
|
slim (4.1.0)
|
||||||
temple (>= 0.7.6, < 0.9)
|
temple (>= 0.7.6, < 0.9)
|
||||||
tilt (>= 2.0.6, < 2.1)
|
tilt (>= 2.0.6, < 2.1)
|
||||||
|
sqlite3 (1.4.4)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
tilt (2.0.10)
|
tilt (2.0.11)
|
||||||
|
tzinfo (2.0.5)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
zeitwerk (2.6.0)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bcrypt (~> 3.1)
|
activesupport (~> 6)
|
||||||
colorize (~> 0.8.1)
|
bcrypt (~> 3)
|
||||||
doc_my_routes!
|
colorize (~> 0.8)
|
||||||
jwt (~> 2.2)
|
doc_my_routes
|
||||||
pg (~> 1.2)
|
dotenv (~> 2)
|
||||||
pry (~> 0.14.1)
|
jwt (~> 2)
|
||||||
sequel (~> 5.43)
|
pry
|
||||||
sinatra (~> 2.1)
|
puma (~> 5)
|
||||||
sinatra-contrib (~> 2.1)
|
rack-test (~> 1)
|
||||||
slim (~> 4.1)
|
rack_csrf (~> 2)
|
||||||
|
sequel (~> 5)
|
||||||
|
sinatra (~> 2)
|
||||||
|
sinatra-contrib (~> 2)
|
||||||
|
slim (~> 4)
|
||||||
|
sqlite3 (~> 1)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.16
|
2.2.16
|
||||||
|
|
111
README.md
111
README.md
|
@ -1,50 +1,103 @@
|
||||||
# LifePEx
|
# LifePex
|
||||||
|
|
||||||
## Setup
|
[![Build Status](https://drone.sceptique.eu/api/badges/Sceptique/LifePex/status.svg)](https://drone.sceptique.eu/Sceptique/LifePex)
|
||||||
|
|
||||||
|
## Install the software on a server
|
||||||
|
|
||||||
|
### Install the dependencies
|
||||||
|
|
||||||
|
You should have the following systems installed: `git` `ruby 3 AND gems` `sqlite OR postgresql`
|
||||||
|
Git is required to clone the project and fetch the tags in order to expose them in the
|
||||||
|
webapp.
|
||||||
|
|
||||||
|
Then make sure you have installed bundler `gem install bundler` if not already done.
|
||||||
|
|
||||||
|
Then install the ruby dependencies.
|
||||||
|
|
||||||
```
|
```
|
||||||
# SQLITE
|
bundle install
|
||||||
./init/database.rb sqlite://sqlite.db
|
```
|
||||||
|
|
||||||
# POSTGRESQL
|
### Configuration setup
|
||||||
|
|
||||||
|
A sample file contains all the variables you will need in order to start the server.
|
||||||
|
This is the `.env.sample` file. You should copy it under the name `.env.local`.
|
||||||
|
|
||||||
|
* LIFEPEX_DB: is an url to your database. It may include a user/password
|
||||||
|
* LIFEPEX_BIND: what your socket will listen to (should probably be 127.0.0.1 or 0.0.0.0)
|
||||||
|
* LIFEPEX_BASE_URL: base url for the whole app (for local usage you can leave if empty)
|
||||||
|
* LIFEPEX_SECRET: you must put randomness in this variable to secure tokens and cookies
|
||||||
|
* LIFEPEX_ENV: may be empty, test, or production to avoid showing the stacktrace when there is a bug
|
||||||
|
|
||||||
|
### Database setup
|
||||||
|
|
||||||
|
The recommanded way is simply to init the database you configured with
|
||||||
|
|
||||||
|
```
|
||||||
|
./init/database.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have postgresql, you may want to create a specific user/password for this.
|
||||||
|
Since I suck with SGDB administration, this is a sample of what you may do.
|
||||||
|
|
||||||
|
**TODO: show how to create a simple attribute with full permission on this database**
|
||||||
|
|
||||||
|
```
|
||||||
psql -U postgres postgres -c "CREATE USER root WITH PASSWORD 'toor' SUPERUSER;"
|
psql -U postgres postgres -c "CREATE USER root WITH PASSWORD 'toor' SUPERUSER;"
|
||||||
psql -U postgres postgres -c "CREATE DATABASE life_pex"
|
psql -U postgres postgres -c "CREATE DATABASE life_pex"
|
||||||
./init/database.rb postgres://root:toor@localhost/life_pex
|
echo LIFEPEX_DB=postgres://root:toor@localhost/life_pex >> .env.local
|
||||||
|
rake db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
## Start
|
### Start
|
||||||
|
|
||||||
|
* configuration: check
|
||||||
|
* setup dependencies: check
|
||||||
|
* setup database: check
|
||||||
|
|
||||||
|
You can simply start the application.
|
||||||
|
|
||||||
```
|
```
|
||||||
export LIFEPEX_SECRET="put something random here"
|
|
||||||
export LIFEPEX_BASE_URL="https://mydomain/base"
|
|
||||||
export LIFEPEX_DB="sqlite://sqlite.db"
|
|
||||||
# you may also want to use postgres or something
|
|
||||||
# export LIFEPEX_DB="postgres://root:toor@localhost/life_pex"
|
|
||||||
./src/app.rb
|
./src/app.rb
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate documentation
|
### Service, update, etc.
|
||||||
|
|
||||||
|
You may want to take a look at the `scripts/` directory which contains tons of config helper, for nginx, systemd, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
![recap image](https://git.sceptique.eu/attachments/ce7c2fab-bd1b-43fc-b10a-21b2c953e2c2)
|
||||||
|
![today image](https://git.sceptique.eu/attachments/ecd7a36d-eac7-40aa-b973-c6ddbac87f51)
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
|
||||||
|
### Generate documentation
|
||||||
|
|
||||||
```
|
```
|
||||||
./init/doc.rb
|
./init/doc.rb
|
||||||
xdg-open ./public/doc/index.html
|
xdg-open ./public/doc/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
### Testing
|
||||||
|
|
||||||
* [x] Register an account
|
Generate first a specific configuration file
|
||||||
* [x] Reset password
|
|
||||||
* [x] Add pex models
|
|
||||||
* [x] Add point each day to each pex model
|
|
||||||
* [x] Make a nice smooth level function
|
|
||||||
* [x] Create a nice CSS template to make it dark and flashy and cyberpunk kikoo
|
|
||||||
* [x] Protect privacy with private pex models
|
|
||||||
* [x] Import CSV script
|
|
||||||
* [x] Make a basic recap graph
|
|
||||||
* [x] Make a niice recap graph
|
|
||||||
* [x] Add an easy and quick way to edit pex
|
|
||||||
* [ ] Add advanced pex with variable value
|
|
||||||
* [ ] Improve security (token validity limit, random seed warning, ...)
|
|
||||||
|
|
||||||
![recap image](https://git.sceptique.eu/attachments/ce7c2fab-bd1b-43fc-b10a-21b2c953e2c2)
|
```
|
||||||
![today image](https://git.sceptique.eu/attachments/ecd7a36d-eac7-40aa-b973-c6ddbac87f51)
|
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 rake db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Then if you want to run the test, simply type `rake test` (you will need the startup env variable to be set first).
|
||||||
|
|
||||||
|
### Debug
|
||||||
|
|
||||||
|
Some debug options can be enabled with the env variable
|
||||||
|
|
||||||
|
`LIFEPEX_ENV=debug`
|
||||||
|
|
21
Rakefile
Normal file
21
Rakefile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require "rake"
|
||||||
|
require "rake/testtask"
|
||||||
|
|
||||||
|
Rake::TestTask.new do |t|
|
||||||
|
t.pattern = "test/*_test.rb"
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace "db" do
|
||||||
|
desc "Migrate the database to the lasted schema"
|
||||||
|
task "migrate" do
|
||||||
|
require_relative "./init/migrate_db"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Reset all tables, schema, data"
|
||||||
|
task "reset" do
|
||||||
|
require_relative "./init/load_env"
|
||||||
|
DB.tables.each {|t| DB.drop_table t }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task default: :test
|
94
documentation/GUIDE.FR.md
Normal file
94
documentation/GUIDE.FR.md
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# Guide utilisateur (French)
|
||||||
|
|
||||||
|
Version du logiciel concernée: v0.15.0.
|
||||||
|
|
||||||
|
*Cette version devrait contenir les mêmes fonctionnalités que la v1.0.0. Il n'y aura donc pas besoin de mise à jour pour la v1.0. Les principales évolutions prévues après cela devraient concerner le design de l'application, ce qui necessitera une mise à jour des images inclues.*
|
||||||
|
|
||||||
|
## Fonctionnalités principales du logiciel
|
||||||
|
|
||||||
|
Le premier but du logiciel est d'offrir une outil pour suivre dans le temps des objectifs. C'est un outil générique, configurable, que l'utilisateur peut adapter à ses besoins. Parmi les usages standards pensés à l'origine on trouve:
|
||||||
|
|
||||||
|
* Suivre son avancement dans un programme sportif (lister des objectifs à cocher)
|
||||||
|
* Ne pas oublier ses erreurs (marquer ses faiblesses pour les surmonter)
|
||||||
|
* Encourager les comportements positifs (ne pas oublier qu'on réussit des choses)
|
||||||
|
|
||||||
|
## License, engagements
|
||||||
|
|
||||||
|
LifePex est un logiciel sous license GPLv3, une license dite "libre". Elle ne comporte aucune garantie que le logiciel lui-même est sans faille ni d'un support quelconque. Il est developpé benevolement. Enfin, vous avez le droit de lire le code source, le modifier et le redistribué, sous reserve que cela soit fait sous license GPLv3 (ou compatible).
|
||||||
|
|
||||||
|
L'administrateur de l'hebergement de LifePex devrait s'engager à ne jamais accéder aux données personnelles stoquées par les utilisateurs de l'instance sans l'accord explicite et individuel de l'utilisateur concerné. Il devrait également s'engager à s'assurer de la mise à jour du logiciel afin de corriger toute faille corrigée pour protéger les utilisateurs qui lui font confiance.
|
||||||
|
|
||||||
|
L'administrateur devrait également s'engager à ne jamais installer de système de traçage de ses utilisateurs. Tout besoin de suivi de charge ou de sécurité ne devrait jamais prendre le pas sur la vie privée de ses utilisateurs.
|
||||||
|
|
||||||
|
## Tour du logiciel
|
||||||
|
|
||||||
|
LifePex est un logiciel qui a avant tout pour objectif d'être utilisé sur mobile.
|
||||||
|
Cependant nous faisons attention à ce que toutes les fonctionnalités soient disponible sur mobile et ordinateur.
|
||||||
|
|
||||||
|
### 1. Barre de navigation (invité)
|
||||||
|
|
||||||
|
Toute page importante du logiciel est accessible via la barre de navigation.
|
||||||
|
Celle-ci est accessible en cliquant sur le bouton en haut à droite de l'écran.
|
||||||
|
|
||||||
|
En temps qu'utilisateur non enregistré vous n'avez pas encore accès à beaucoup de pages.
|
||||||
|
Cliquez sur s'enregistrer ("Register") pour passer à la suite.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/00c81464-06bd-446d-bbb8-f347bc574d2d)
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Enregistrement d'un nouveau compte
|
||||||
|
|
||||||
|
La première étape de la création d'un nouveau compte est d'entrer un nom d'utilisateur que vous voulez utiliser pour s'identifier, et un mot de passe que seul vous pouvez connaitre. Cela vous permettra l'empêcher tout autre utilisateur d'accéder à vos données. Le boutton en fin de page permet de valider la création de son compte.
|
||||||
|
Cela vous connectera automatiquement.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/5dc654cd-f8aa-44ea-b7e5-8f3b0bf4eb67)
|
||||||
|
|
||||||
|
*Attention: le logiciel ne dispose pas de votre addresse email à ce stade, il est donc impossible de retrouver un mot de passe oublié.*
|
||||||
|
|
||||||
|
### 3. Connexion à un compte existant
|
||||||
|
|
||||||
|
Si vous avez déjà un compte cependant, vous pouvez également utiliser le bouton de connexion dans la barre de navigation pour accéder à la page correspondante. Entrez simplement votre nom d'utilisateur et mot de passe renseigné lors de la création de votre compte pour accéder à votre compte et validez le formulaire.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/85e9b4cb-de9b-4335-ba5b-2f1b94557606)
|
||||||
|
|
||||||
|
### 4. Page principale (aujourd'hui)
|
||||||
|
|
||||||
|
La page principale vous donne accès à votre tableau de bord. C'est là que vous pouvez valider un point dans une des lignes (parfois ppellées "pex"). Appuyez sur le bouton + ou - de la ligne pour augmenter le nombre de validations de la journée.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/ba3fd0b0-1a72-496b-aec5-9fe46541b83f)
|
||||||
|
|
||||||
|
#### 4.1 Ajout d'un nouveau pex
|
||||||
|
|
||||||
|
Si vous souhaitez ajouter une nouvelle ligne, il est possible d'appuyer sur le gros boutton vert en bas à droit de la page principale. Cela vous amènera sur un formulaire contenant un champs pour le nom, la catégorie, et le nombre de "xp" que chaque validation de ce pex vous apportera. Les xp sont une valeur relative utilisée pour comparer la valeur de 2 pex.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/bcf313ec-ee32-4a20-ba86-c2d24ce91426)
|
||||||
|
|
||||||
|
### 5. Barre de navigation (avec un compte)
|
||||||
|
|
||||||
|
La barre de navigation, toujours accessible via le boutton en haut à droite de l'écran permet maintenant d'accéder à de nombreuses pages.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/890c50a1-c6d1-4474-b756-8e6d5cdac76b)
|
||||||
|
|
||||||
|
### 6. Récapitulatif
|
||||||
|
|
||||||
|
La page de récapitulatif, via la barre de navigation et le boutton "Recap" permet d'avoir accès en 1 coup d'oeil plusieurs informations:
|
||||||
|
|
||||||
|
* Votre niveau et votre xp accumulé en se basant sur les "xp" évoqué en #4.1
|
||||||
|
|
||||||
|
* Le progrès dans le temps de vos différentes catégories. Il est possible de cliquer sur la légende pour faire apparaitre ou disparaitre une des ligne du graphique.
|
||||||
|
|
||||||
|
* Vos accomplissements validés et leurs médailles respectives.
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/ddc33b93-f660-4991-87f4-7e4e6a382e16)
|
||||||
|
|
||||||
|
### 7. Les accomplissements
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/1803bf3b-633f-49bb-a079-0adf7b12f03c)
|
||||||
|
|
||||||
|
### 8. Ajout d'accomplissement
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/88362fae-0823-4373-9861-82872057a8a0)
|
||||||
|
|
||||||
|
### 9. Page principale, options avancées
|
||||||
|
|
||||||
|
![](https://git.sceptique.eu/attachments/abf52606-f1b4-45cd-9ca6-53ef7b33c889)
|
35
documentation/SECURITY.md
Normal file
35
documentation/SECURITY.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Security
|
||||||
|
|
||||||
|
## List of important things
|
||||||
|
|
||||||
|
* Actors
|
||||||
|
* Users: uses probably a web browser
|
||||||
|
* Administrator: host data and source code
|
||||||
|
* Software
|
||||||
|
* SQL Database: external software that holds data
|
||||||
|
* Ruby and dependencies: source code from open source external software
|
||||||
|
* Bootstrap CDN: css and javascript from external source
|
||||||
|
* Hardware
|
||||||
|
* Host server
|
||||||
|
* Sensitive data
|
||||||
|
* Private user's data
|
||||||
|
|
||||||
|
## Trust, privacy, threat model
|
||||||
|
|
||||||
|
* 1. Because we handle private data, it is important to protect it against leak.
|
||||||
|
User's private data should not be exposed publicly, and it should not be
|
||||||
|
possible to impersonate users.
|
||||||
|
|
||||||
|
* 2. Data inputed should not include financial information, making it
|
||||||
|
less a target for attackers. It should also not include many personnal
|
||||||
|
information, so even in case of security leak, exploiting data should be harder.
|
||||||
|
However, it is possible that the user find interest into putting sexual,
|
||||||
|
religious or medical information in the database. This is might be dangerous
|
||||||
|
for individuals if they are not aware of the point 3.
|
||||||
|
|
||||||
|
* 3. Because of the way it is currently used, the administrator has a complete
|
||||||
|
technical control over the data which are very easy to extract and export.
|
||||||
|
The administrator is probably close from some of the users and may have a
|
||||||
|
interest into looking these easly accessible private data.
|
||||||
|
The user must consider the administrator trustworthy of the information he
|
||||||
|
will input or not fear them to be read by the administrator.
|
13
init/load_env.rb
Normal file
13
init/load_env.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require "sequel"
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
|
require_relative "../src/utils/env"
|
||||||
|
require_relative "../src/utils/semver"
|
||||||
|
|
||||||
|
load_dotenv
|
||||||
|
DB = Sequel.connect ENV["LIFEPEX_DB"]
|
||||||
|
|
||||||
|
if ENV["environment"] == "debug"
|
||||||
|
require "pry"
|
||||||
|
binding.pry
|
||||||
|
end
|
|
@ -1,38 +1,30 @@
|
||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
require "sequel"
|
require_relative "./load_env"
|
||||||
require "colorize"
|
|
||||||
require_relative "../src/utils/semver.rb"
|
|
||||||
|
|
||||||
DB = Sequel.connect ARGV[0]
|
|
||||||
|
|
||||||
if ARGV[1] == "debug"
|
|
||||||
require "pry"
|
|
||||||
binding.pry
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_version
|
def current_version
|
||||||
DB[:meta].first[:version] rescue 0
|
DB[:meta].first[:version] rescue 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def migrate(version, &block)
|
def migrate(version, message = nil, &block)
|
||||||
puts "Check migration #{version}"
|
puts "Check migration #{version}".on_blue
|
||||||
if current_version < version
|
if current_version < version
|
||||||
puts "Migrate #{version}".blue
|
puts "Migrate #{version}".blue
|
||||||
begin
|
begin
|
||||||
yield
|
yield
|
||||||
DB[:meta].update(version: version)
|
DB[:meta].update(version: version)
|
||||||
puts "Successfuly set version #{version}"
|
puts "Successfuly set version #{version}".green
|
||||||
|
puts message.green if message
|
||||||
rescue => err
|
rescue => err
|
||||||
puts err.message.red
|
puts err.message.on_red
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
puts "Already migrated #{version}".blue
|
puts "Already migrated #{version}".yellow
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 1 do
|
migrate 1, "Initialized database" do
|
||||||
DB.create_table :meta do
|
DB.create_table :meta do
|
||||||
primary_key :id
|
primary_key :id
|
||||||
Int :version
|
Int :version
|
||||||
|
@ -57,33 +49,29 @@ migrate 1 do
|
||||||
String :username
|
String :username
|
||||||
String :hashed_password
|
String :hashed_password
|
||||||
end rescue puts "users already exists".yellow
|
end rescue puts "users already exists".yellow
|
||||||
puts "Initialized database".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 2 do
|
migrate 2, "Add pex categories" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :category, String
|
add_column :category, String
|
||||||
end rescue puts "pexs.category already exists".yellow
|
end rescue puts "pexs.category already exists".yellow
|
||||||
puts "Migrated categories".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 3 do
|
migrate 3, "Add pex user belonging" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :user_id, :Int
|
add_column :user_id, :Int
|
||||||
end rescue puts "pexs.user_id already exists".yellow
|
end rescue puts "pexs.user_id already exists".yellow
|
||||||
DB[:pexs].update(user_id: 1)
|
DB[:pexs].update(user_id: 1)
|
||||||
puts "Migrated pex belonging to user".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 4 do
|
migrate 4, "Fix pex category default behavior" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
set_column_default(:category, '')
|
set_column_default(:category, '')
|
||||||
end rescue puts "pexs.category default already exists".yellow
|
end rescue puts "pexs.category default already exists".yellow
|
||||||
DB[:pexs].where(category: nil).update(category: '')
|
DB[:pexs].where(category: nil).update(category: '')
|
||||||
puts "Migrated pex default category".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 5 do
|
migrate 5, "Add meta schema" do
|
||||||
DB.alter_table :meta do
|
DB.alter_table :meta do
|
||||||
add_column :code_version, String
|
add_column :code_version, String
|
||||||
end rescue puts "meta.code_version already exists".yellow
|
end rescue puts "meta.code_version already exists".yellow
|
||||||
|
@ -94,10 +82,51 @@ migrate 6 do
|
||||||
DB.alter_table :meta do
|
DB.alter_table :meta do
|
||||||
add_column :code_date, String
|
add_column :code_date, String
|
||||||
end rescue puts "meta.code_date already exists".yellow
|
end rescue puts "meta.code_date already exists".yellow
|
||||||
DB[:meta].update(code_version: "0")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
puts "End migration".green
|
migrate 7, "Fix pex initial category" do
|
||||||
|
DB[:pexs].each do |pex|
|
||||||
|
DB[:pexs].where(id: pex[:id]).update(category: pex[:category].to_s.downcase)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
migrate 8, "Add default hidden for pexs" do
|
||||||
|
DB.alter_table :pexs do
|
||||||
|
add_column :hidden, TrueClass
|
||||||
|
end rescue puts "pex.hidden already exists".yellow
|
||||||
|
DB[:pexs].each { |pex| DB[:pexs].where(id: pex[:id]).update(hidden: false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
migrate 9, "Add achievements" do
|
||||||
|
DB.create_table :achievements do
|
||||||
|
primary_key :id
|
||||||
|
Int :user_id
|
||||||
|
String :name
|
||||||
|
String :success_name
|
||||||
|
String :parameters_json
|
||||||
|
String :icon
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
migrate 10, "Add generic flag to pexs, for bookmarking" do
|
||||||
|
DB.alter_table :pexs do
|
||||||
|
add_column :flag, :String
|
||||||
|
end rescue puts "pexs.flag already exists".yellow
|
||||||
|
end
|
||||||
|
|
||||||
|
migrate 11, "Add recalls" do
|
||||||
|
DB.create_table :recalls do
|
||||||
|
primary_key :id
|
||||||
|
Int :user_id
|
||||||
|
Int :pex_id
|
||||||
|
String :name
|
||||||
|
Int :span_duration
|
||||||
|
Int :repeated
|
||||||
|
end
|
||||||
|
puts "Migrated recalls".green
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "End migration".on_blue
|
||||||
CODE_VERSION = `git tag`.split("\n").map{ |str| Utils::Semver.new(str) }.sort.last.to_s
|
CODE_VERSION = `git tag`.split("\n").map{ |str| Utils::Semver.new(str) }.sort.last.to_s
|
||||||
CODE_DATE=`git show #{CODE_VERSION} --pretty="format:%as"`.split("\n").first
|
CODE_DATE=`git show #{CODE_VERSION} --pretty="format:%as"`.split("\n").first
|
||||||
DB[:meta].update(code_version: CODE_VERSION)
|
DB[:meta].update(code_version: CODE_VERSION)
|
39
public/css/bootstrap-override.css
vendored
39
public/css/bootstrap-override.css
vendored
|
@ -16,7 +16,46 @@
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-bookmarked {
|
||||||
|
background: #332;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background: #ff00;
|
background: #ff00;
|
||||||
border-color: #ff00;
|
border-color: #ff00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .dropdown-menu[data-bs-popper] { */
|
||||||
|
/* left: -100%; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* @media (min-width: 374px) { */
|
||||||
|
/* .navbar-expand-xs { */
|
||||||
|
/* flex-wrap: nowrap; */
|
||||||
|
/* justify-content: flex-start; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-nav { */
|
||||||
|
/* flex-direction: row; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-nav .dropdown-menu { */
|
||||||
|
/* position: absolute; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-nav .nav-link { */
|
||||||
|
/* padding-right: 0.5rem; */
|
||||||
|
/* padding-left: 0.5rem; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-nav-scroll { */
|
||||||
|
/* overflow: visible; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-collapse { */
|
||||||
|
/* display: flex !important; */
|
||||||
|
/* flex-basis: auto; */
|
||||||
|
/* } */
|
||||||
|
/* .navbar-expand-xs .navbar-toggler { */
|
||||||
|
/* display: none; */
|
||||||
|
/* } */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* .navbar-nav { */
|
||||||
|
/* flex-direction: row; */
|
||||||
|
/* } */
|
||||||
|
|
|
@ -70,6 +70,10 @@ h2 {
|
||||||
height: 45px;
|
height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smaller {
|
||||||
|
font-size: x-small !important;
|
||||||
|
}
|
||||||
|
|
||||||
.highcharts-figure, .highcharts-data-table table {
|
.highcharts-figure, .highcharts-data-table table {
|
||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|
0
public/doc/.keep
Normal file
0
public/doc/.keep
Normal file
BIN
public/img/favicon.png
Normal file
BIN
public/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
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,16 +17,99 @@ 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 name = t.attributes.name.value;
|
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) => {
|
||||||
|
if (v.length == 2) 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]));
|
||||||
|
const date = parseCookie(document.cookie).date || new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
ajax({
|
||||||
|
method: fields_attributes["type"] == "-" ? "DELETE" : "POST",
|
||||||
|
url: `/api/user-pex/v1/pexs/${fields_attributes["id"]}/validation`,
|
||||||
|
body: JSON.stringify({ date: date, force_count_total: true }),
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
// === Sample how to make dynamic partial replacement ===
|
||||||
|
// const target_line = event.target.parentNode.parentElement;
|
||||||
|
// ajax({
|
||||||
|
// method: 'GET',
|
||||||
|
// url: `/api/pex/v2/pexs/${fields_attributes["id"]}/more`,
|
||||||
|
// headers: { Accept: "text/html;*/*" },
|
||||||
|
// on_success: (body) => {
|
||||||
|
// target_line.innerHTML = body;
|
||||||
|
// const toggler = target_line.querySelector('.pex-editor-toggler');
|
||||||
|
// setupPexEditorToggler(toggler);
|
||||||
|
// },
|
||||||
|
// on_failure: (body, req) => {
|
||||||
|
// flashError("Error JS#0002 while rendering...");
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
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...");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPexEditorToggler(toggler) {
|
||||||
|
const name = toggler.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);
|
||||||
show(t);
|
show(toggler);
|
||||||
t.addEventListener("click", (event) => {
|
toggler.addEventListener("click", (event) => {
|
||||||
toggle(pex_editor);
|
toggle(pex_editor);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (_event) => {
|
||||||
|
const editor_togglers = document.querySelectorAll('.pex-editor-toggler');
|
||||||
|
editor_togglers.forEach(setupPexEditorToggler);
|
||||||
|
|
||||||
|
const userpexvalidation0 = document.querySelectorAll('.userpexvalidationvalue').map().filter(tag => tag.textContent == "0");
|
||||||
|
userpexvalidation0.forEach((tag) => {
|
||||||
|
tag.parentNode.parentNode.childNodes[2].childNodes[0].hidden = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
function setupChart(...cumuls) {
|
function setupChart({ all_dates }, ...cumuls) {
|
||||||
const chart = Highcharts.chart('recap-xp-container', {
|
const chart = Highcharts.chart('recap-xp-container', {
|
||||||
chart: {
|
chart: {
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
|
@ -7,24 +7,53 @@ function setupChart(...cumuls) {
|
||||||
text: '',
|
text: '',
|
||||||
color: '#55f5f5',
|
color: '#55f5f5',
|
||||||
},
|
},
|
||||||
series: cumuls.map(cumul => ({
|
xAxis: {
|
||||||
|
categories: all_dates,
|
||||||
|
},
|
||||||
|
series: cumuls.map((cumul, idx) => ({
|
||||||
// color: '#55f5f5',
|
// color: '#55f5f5',
|
||||||
|
visible: idx != 0,
|
||||||
name: cumul.name,
|
name: cumul.name,
|
||||||
data: cumul.data,
|
data: cumul.data,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
function objectToQueryParam(obj) {
|
||||||
const xhttp = new XMLHttpRequest();
|
return Object.entries(obj).map(tuple => tuple.join("=")).join("&");
|
||||||
xhttp.onreadystatechange = () => {
|
|
||||||
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
|
||||||
const json_output = JSON.parse(xhttp.responseText);
|
|
||||||
setupChart(...json_output.pex_tables);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
xhttp.open("GET", "/api/pex/v1/recap", true);
|
function urlWithQueryParams(base, object_with_params) {
|
||||||
xhttp.setRequestHeader("Accept", "application/json");
|
if (Object.keys(object_with_params).length > 0) {
|
||||||
xhttp.send();
|
return `${base}?${objectToQueryParam(object_with_params)}`;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestChart() {
|
||||||
|
let url = "/api/pex/v1/recap";
|
||||||
|
const days_ago = document.querySelector("#inputDaysAgo").value;
|
||||||
|
if (days_ago.length > 0) {
|
||||||
|
url = urlWithQueryParams(url, { days_ago });
|
||||||
|
}
|
||||||
|
ajax({
|
||||||
|
method: "GET",
|
||||||
|
url: url,
|
||||||
|
body: null,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
const json_output = JSON.parse(body);
|
||||||
|
setupChart({ all_dates: json_output.all_dates }, ...json_output.pex_tables);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function () {
|
||||||
|
requestChart();
|
||||||
|
|
||||||
|
const form = document.querySelector(".recap-xp form#reloader");
|
||||||
|
form.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
requestChart();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
10
scripts/README.md
Normal file
10
scripts/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Scripts
|
||||||
|
|
||||||
|
In the scripts we are supposed to do everything as the user and group `lifepex`.
|
||||||
|
The software is supposed to be cloned in `/srv/LifePex`.
|
||||||
|
When copying a script, make sure you update the variables.
|
||||||
|
|
||||||
|
* `update.sh` pull the lastest source code and ensure the database is migrated
|
||||||
|
the lastest schema.
|
||||||
|
* `lifepex.service` is usable for installing a service in systemd.
|
||||||
|
* `nginx.conf` it is a possible configuration for nginx reverse proxy.
|
17
scripts/lifepex.service
Normal file
17
scripts/lifepex.service
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[Unit]
|
||||||
|
Description=LifePex
|
||||||
|
Documentation=https://git.sceptique.eu/Sceptique/LifePex
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/env ruby /srv/LifePex/src/app.rb
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
User=lifepex
|
||||||
|
Group=lifepex
|
||||||
|
WorkingDirectory=/srv/LifePex
|
||||||
|
Environment=PORT=8080
|
||||||
|
Environment=LIFEPEX_BASE_URL="https://lifepex.mydomain.org"
|
||||||
|
Environment=LIFEPEX_DB="sqlite://sqlite.db"
|
||||||
|
#Environment=LIFEPEX_SECRET=""
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
10
scripts/nginx.conf
Normal file
10
scripts/nginx.conf
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name lifepex.sceptique.eu;
|
||||||
|
|
||||||
|
include reverse_proxy.conf;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:10007;
|
||||||
|
}
|
||||||
|
}
|
13
scripts/update.sh
Normal file
13
scripts/update.sh
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Your env should already be setup before you run this
|
||||||
|
|
||||||
|
cd /srv/LifePex/
|
||||||
|
git checkout master
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/master --tags
|
||||||
|
bundle install
|
||||||
|
./init/database.rb
|
||||||
|
mkdir -p ./public/doc
|
||||||
|
./init/doc.rb
|
||||||
|
sudo systemctl restart lifepex
|
55
src/achievements/achievement.rb
Normal file
55
src/achievements/achievement.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
require_relative "./dsl"
|
||||||
|
|
||||||
|
module LifePex::AchievementDSL::Includer
|
||||||
|
extend LifePex::AchievementDSL
|
||||||
|
SUCCESS_LIST = []
|
||||||
|
|
||||||
|
SUCCESS_LIST << success("Marked a perk regularly") do
|
||||||
|
description "This success search for a serie of pexs in a span of time"
|
||||||
|
parameter "DaysAmountTotal" do
|
||||||
|
type :Integer
|
||||||
|
required
|
||||||
|
description "Duration in days to eval if the criteria are valids"
|
||||||
|
end
|
||||||
|
parameter "DaysAmountValidity" do
|
||||||
|
type :Integer
|
||||||
|
required
|
||||||
|
description "Amount of day over the total to validate the success"
|
||||||
|
end
|
||||||
|
parameter "MinimumAmountMarkedByDay" do
|
||||||
|
type :Integer
|
||||||
|
required
|
||||||
|
description "Minimum amount of mark for a day to be validated"
|
||||||
|
end
|
||||||
|
# parameter "PexIds" do
|
||||||
|
# type :Array
|
||||||
|
# description "List of pex that validates"
|
||||||
|
# end
|
||||||
|
parameter "Category" do
|
||||||
|
type :String
|
||||||
|
description "A category that validates"
|
||||||
|
end
|
||||||
|
|
||||||
|
to_complete do |user_pexs, pexs|
|
||||||
|
allowed_pex_ids = given(:PexIds)
|
||||||
|
allowed_pex_ids = pexs.filter { |pex| pex[:category] == given(:Category) }.map { |pex| pex[:id] } if given(:Category)
|
||||||
|
applicable = user_pexs.filter { |up| allowed_pex_ids.include?(up[:pex_id]) }
|
||||||
|
is_valid = false
|
||||||
|
counter = []
|
||||||
|
applicable.each do |up|
|
||||||
|
while !counter.empty? && (up[:created_at] - counter.first[:created_at]) > given(:DaysAmountTotal)
|
||||||
|
counter.shift
|
||||||
|
end unless counter.empty?
|
||||||
|
counter << up
|
||||||
|
grouped = counter.group_by { |c| c[:created_at] }
|
||||||
|
if grouped.values.filter { |tuple| tuple.size >= given(:MinimumAmountMarkedByDay) }.size >= given(:DaysAmountValidity)
|
||||||
|
is_valid = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
is_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SUCCESS_INDEX = SUCCESS_LIST.map { |s| [s.name, s] }.to_h
|
||||||
|
end
|
49
src/achievements/dsl.rb
Normal file
49
src/achievements/dsl.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
module LifePex::AchievementDSL
|
||||||
|
UNDEFINED = :undefined_by_dsl
|
||||||
|
|
||||||
|
# @example
|
||||||
|
# include LifePex::AchievementDSL::Dslize
|
||||||
|
# dslize_input :name
|
||||||
|
# dslize_flag :required
|
||||||
|
module Dslize
|
||||||
|
def self.included(m)
|
||||||
|
def m.dslize_input(name)
|
||||||
|
define_method name do |v = UNDEFINED|
|
||||||
|
if v == :undefined_by_dsl
|
||||||
|
instance_variable_get "@#{name}"
|
||||||
|
else
|
||||||
|
instance_variable_set "@#{name}", v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def m.dslize_flag(name)
|
||||||
|
define_method "is_#{name}" do
|
||||||
|
instance_variable_get "@#{name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method name do
|
||||||
|
instance_variable_set "@#{name}", true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require_relative "./success.rb"
|
||||||
|
|
||||||
|
module LifePex::AchievementDSL
|
||||||
|
AVAILABLE_GENERIC_SUCCESS = {}
|
||||||
|
AVAILABLE_SETUP_SUCCESS = {}
|
||||||
|
|
||||||
|
def success(name, &block)
|
||||||
|
Success.new(name, &block)
|
||||||
|
# if block_given?
|
||||||
|
# s = Success.new(name, &block)
|
||||||
|
# LifePex::AchievementDSL::AVAILABLE_GENERIC_SUCCESS[name] = s
|
||||||
|
# s
|
||||||
|
# else
|
||||||
|
# LifePex::AchievementDSL::AVAILABLE_GENERIC_SUCCESS[name]
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
72
src/achievements/success.rb
Normal file
72
src/achievements/success.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
class LifePex::AchievementDSL::Success
|
||||||
|
require_relative "./success/parameter.rb"
|
||||||
|
require_relative "./success/setup.rb"
|
||||||
|
|
||||||
|
include LifePex::AchievementDSL::Dslize
|
||||||
|
dslize_input :name
|
||||||
|
dslize_input :description
|
||||||
|
attr_reader :parameters
|
||||||
|
|
||||||
|
def initialize(name, &block)
|
||||||
|
@name = name
|
||||||
|
@parameters = []
|
||||||
|
@to_complete = nil
|
||||||
|
@description = nil
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameter(name, type = nil, &block)
|
||||||
|
@parameters << Parameter.new(name, type, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_complete(&block)
|
||||||
|
@to_complete = block
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_to_complete
|
||||||
|
@to_complete
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup(name, to: 'default', &block)
|
||||||
|
Setup.new(self, name, &block)
|
||||||
|
# LifePex::AchievementDSL::AVAILABLE_SETUP_SUCCESS[to] ||= {}
|
||||||
|
# if block_given?
|
||||||
|
# s = Setup.new(self, name, &block)
|
||||||
|
# LifePex::AchievementDSL::AVAILABLE_SETUP_SUCCESS[to][name] = s
|
||||||
|
# s
|
||||||
|
# else
|
||||||
|
# LifePex::AchievementDSL::AVAILABLE_SETUP_SUCCESS[to][name]
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
def static_cast_parameter_value(value: "", type: :String)
|
||||||
|
if type == :Integer
|
||||||
|
Integer(value)
|
||||||
|
elsif type == :Number || type == :Float
|
||||||
|
Float(value)
|
||||||
|
elsif type == :ArrayOfString
|
||||||
|
value.split(',')
|
||||||
|
elsif type == :ArrayOfInteger
|
||||||
|
raise "Invalid ArrayOfInteger" unless value =~ /^((\d+){1}?,?)*$/
|
||||||
|
value.split(',').map(&:to_i)
|
||||||
|
elsif type == :ArrayOfNumber
|
||||||
|
raise "Invalid ArrayOfNumber" unless value =~ /^((\d+(\.\d+)?){1}?,?)*$/
|
||||||
|
value.split(',').map(&:to_f)
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_parameters_hash(parameters)
|
||||||
|
parameters.map do |key, value|
|
||||||
|
parameter = @parameters.find { |param| param.name == key }
|
||||||
|
raise RuntimeError.new "No #{key} parameter found" if parameter.nil?
|
||||||
|
begin
|
||||||
|
typed_value = static_cast_parameter_value(value: value, type: parameter.type)
|
||||||
|
rescue => err
|
||||||
|
raise "Invalid value \"#{value}\" for type #{parameter.type} of parameter \"#{parameter.name}\""
|
||||||
|
end
|
||||||
|
[key, typed_value]
|
||||||
|
end.to_h
|
||||||
|
end
|
||||||
|
end
|
24
src/achievements/success/parameter.rb
Normal file
24
src/achievements/success/parameter.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
class LifePex::AchievementDSL::Success::Parameter
|
||||||
|
include LifePex::AchievementDSL::Dslize
|
||||||
|
dslize_input :name
|
||||||
|
dslize_input :type
|
||||||
|
dslize_input :description
|
||||||
|
dslize_flag :required
|
||||||
|
|
||||||
|
def initialize(name, type = nil, &block)
|
||||||
|
@name = name
|
||||||
|
@type = type
|
||||||
|
@description = nil
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
# not dsl related
|
||||||
|
def accept?(v)
|
||||||
|
return true if @input_type.nil?
|
||||||
|
return true if @input_type == :Numeric && v.is_a?(Numeric)
|
||||||
|
return true if @input_type == :Integer && v.is_a?(Numeric) && v.round(0) == v
|
||||||
|
return true if @input_type == :String && v.is_a?(String)
|
||||||
|
return true if @input_type == :Array && v.is_a?(Array)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
48
src/achievements/success/setup.rb
Normal file
48
src/achievements/success/setup.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
class LifePex::AchievementDSL::Success::Setup
|
||||||
|
include LifePex::AchievementDSL::Dslize
|
||||||
|
dslize_input :icon
|
||||||
|
dslize_input :name
|
||||||
|
|
||||||
|
attr_reader :success # FIXME: why cannot put success as dsl input ?
|
||||||
|
attr_reader :parameters
|
||||||
|
|
||||||
|
def initialize(success, name, &block)
|
||||||
|
@success = success
|
||||||
|
@name = name
|
||||||
|
@parameters = {}
|
||||||
|
@icon = nil
|
||||||
|
instance_eval(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameter(**kv)
|
||||||
|
param_name = kv.keys.first.to_s
|
||||||
|
param_value = kv.values.first
|
||||||
|
param = @success.parameters.find { |param| param.name == param_name }
|
||||||
|
raise RuntimeError.new "Invalid value #{param_value} for #{param_name}" unless param_value.nil? && !param.is_required || param.accept?(param_value)
|
||||||
|
@parameters[param_name] = param_value
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(user_pexs, pexs)
|
||||||
|
to_complete = @success.get_to_complete
|
||||||
|
instance_exec user_pexs, pexs, &to_complete
|
||||||
|
end
|
||||||
|
|
||||||
|
def given(name)
|
||||||
|
@parameters[name.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
|
module Medal
|
||||||
|
def medal
|
||||||
|
if self.icon == "gold"
|
||||||
|
"🥇"
|
||||||
|
elsif self.icon == "silver"
|
||||||
|
"🥈"
|
||||||
|
elsif self.icon == "copper" || self.icon == "bronze"
|
||||||
|
"🥉"
|
||||||
|
else
|
||||||
|
"🏅"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
include Medal
|
||||||
|
end
|
116
src/app.rb
116
src/app.rb
|
@ -1,61 +1,100 @@
|
||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
require "pry"
|
|
||||||
# require "sinatra"
|
|
||||||
require "sinatra/cookies"
|
|
||||||
require "sinatra/base"
|
|
||||||
require "sinatra/namespace"
|
|
||||||
require "slim"
|
|
||||||
require "bcrypt"
|
|
||||||
require "jwt"
|
|
||||||
require "sequel"
|
|
||||||
require "json"
|
|
||||||
require "doc_my_routes"
|
|
||||||
|
|
||||||
require_relative "./utils/url.rb"
|
|
||||||
|
|
||||||
module LifePex
|
module LifePex
|
||||||
DB = Sequel.connect ARGV[0] || ENV["LIFEPEX_DB"] || "sqlite://sqlite.db"
|
end
|
||||||
BASE_URL = ARGV[1] || ENV["LIFEPEX_BASE_URL"] || "http://localhost:4567"
|
|
||||||
SECRET = ENV['LIFEPEX_SECRET'] || "LifePexSecret"
|
require "sinatra/base" # web
|
||||||
puts "WARNING: Your secret is NOT very secret, thing about changing the LIFEPEX_SECRET" if SECRET == "LifePexSecret"
|
require "sinatra/cookies"
|
||||||
|
require "slim"
|
||||||
|
require "sinatra/namespace" # api
|
||||||
|
require "doc_my_routes" # api doc
|
||||||
|
require "bcrypt" # security
|
||||||
|
require "jwt"
|
||||||
|
require "rack/csrf"
|
||||||
|
require "securerandom"
|
||||||
|
require "sequel" # db
|
||||||
|
require "json" # helpers
|
||||||
|
require "active_support"
|
||||||
|
require "active_support/core_ext"
|
||||||
|
require "pry" # debug
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
require_relative "./utils/env"
|
||||||
|
module LifePex
|
||||||
|
APP_ENV = load_dotenv
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initialize framework
|
||||||
|
require_relative "./utils/url.rb"
|
||||||
|
require_relative "./utils/boot_framework"
|
||||||
|
LifePex::BootFramework.boot_application do
|
||||||
|
harshly_need_env "LIFEPEX_DB"
|
||||||
|
kindly_ask_env "LIFEPEX_BASE_URL"
|
||||||
|
kindly_ask_env "LIFEPEX_SECRET" do
|
||||||
|
warning "\"LIFEPEX_SECRET\" will be randomly generated as a fallback"
|
||||||
|
ENV["LIFEPEX_SECRET"] = SecureRandom.hex(64)
|
||||||
|
end
|
||||||
|
harshly_do env["LIFEPEX_SECRET"].size < 8 do
|
||||||
|
error "Your secret is NOT very secret, fix this (at least 8 hexadigits entropy)"
|
||||||
|
end
|
||||||
|
end.finish!
|
||||||
|
|
||||||
|
# Setup module variables
|
||||||
|
module LifePex
|
||||||
|
DB = Sequel.connect ENV["LIFEPEX_DB"]
|
||||||
|
BASE_URL = ENV["LIFEPEX_BASE_URL"] || "http://localhost:4567"
|
||||||
|
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]
|
||||||
|
|
||||||
include LifePex::Utils::Url
|
include LifePex::Utils::Url
|
||||||
|
|
||||||
module Systems
|
module Systems
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require_relative "./utils/json_api.rb"
|
# Then we load all the systems
|
||||||
|
|
||||||
require_relative "./models/level.rb"
|
Dir[File.join(__dir__, 'utils', '*.rb')].each { |file| require file }
|
||||||
require_relative "./models/user.rb"
|
|
||||||
require_relative "./models/pex.rb"
|
|
||||||
require_relative "./models/user_pex.rb"
|
|
||||||
|
|
||||||
require_relative "./systems/api_list.rb"
|
require_relative "./achievements/dsl.rb"
|
||||||
require_relative "./systems/auth.rb"
|
require_relative "./achievements/achievement.rb"
|
||||||
require_relative "./systems/user.rb"
|
|
||||||
require_relative "./systems/pex.rb"
|
|
||||||
require_relative "./systems/pex2.rb"
|
|
||||||
|
|
||||||
|
Dir[File.join(__dir__, 'models', '*.rb')].each { |file| require file }
|
||||||
|
Dir[File.join(__dir__, 'systems', '*.rb')].each { |file| require file }
|
||||||
|
|
||||||
|
# Static file serving in this file because it is overkill to create a file for this
|
||||||
class LifePex::Systems::PublicSystem < Sinatra::Base
|
class LifePex::Systems::PublicSystem < Sinatra::Base
|
||||||
set :public_folder, 'public'
|
set :public_folder, "public"
|
||||||
end
|
end
|
||||||
|
|
||||||
class LifePex::Systems::BaseSystem < Sinatra::Base
|
# Main app
|
||||||
|
class LifePex::App < Sinatra::Base
|
||||||
DocMyRoutes.configure do |config|
|
DocMyRoutes.configure do |config|
|
||||||
config.title = "LifePex"
|
config.title = "LifePex"
|
||||||
config.description = "LifePex JSON REST API documentation"
|
config.description = "LifePex JSON REST API documentation"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
set :session_secret, LifePex::SECRET
|
||||||
enable :sessions
|
enable :sessions
|
||||||
use LifePex::Systems::AuthSystem
|
|
||||||
use LifePex::Systems::PublicSystem
|
# use Rack::Csrf, skip: ["POST:/api/*"], :raise => (LifePex::APP_ENV != "production") if LifePex::APP_ENV != "test"
|
||||||
use LifePex::Systems::UserSystem
|
set :protection, :except => :json_csrf if LifePex::APP_ENV != "test"
|
||||||
use LifePex::Systems::PexSystem
|
|
||||||
use LifePex::Systems::Pex2System
|
LifePex::Systems.constants
|
||||||
|
.filter { |system| system.to_s =~ /System$/ }
|
||||||
|
.each { |system|
|
||||||
|
use LifePex::Systems.const_get(system)
|
||||||
|
puts "Loaded #{system.to_s.green}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# use LifePex::Systems::AuthSystem
|
||||||
|
# use LifePex::Systems::PublicSystem
|
||||||
|
# use LifePex::Systems::UserSystem
|
||||||
|
# use LifePex::Systems::PexSystem
|
||||||
|
# use LifePex::Systems::Pex2System
|
||||||
|
# use LifePex::Systems::AchievementSystem
|
||||||
|
# use LifePex::Systems::UserPexSystem
|
||||||
|
|
||||||
include JSON::API
|
include JSON::API
|
||||||
not_found do
|
not_found do
|
||||||
|
@ -66,9 +105,14 @@ class LifePex::Systems::BaseSystem < Sinatra::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/meta/v1" do
|
get "/api/meta/v1", provides: "json" do
|
||||||
LifePex::Systems::ApiList.get_all_api_routes.to_json
|
LifePex::Systems::ApiList.get_all_api_routes.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
set :bind, ENV["LIFEPEX_BIND"] || "127.0.0.1"
|
||||||
|
|
||||||
|
set :environment, LifePex::APP_ENV
|
||||||
|
ENV["RACK_ENV"] = LifePex::APP_ENV
|
||||||
|
|
||||||
run! if app_file == $0
|
run! if app_file == $0
|
||||||
end
|
end
|
||||||
|
|
38
src/models/achievement.rb
Normal file
38
src/models/achievement.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
class LifePex::Achievement < Sequel::Model(LifePex::DB[:achievements])
|
||||||
|
include LifePex::AchievementDSL::Success::Setup::Medal
|
||||||
|
many_to_one :user
|
||||||
|
|
||||||
|
# def validate
|
||||||
|
# super
|
||||||
|
# end
|
||||||
|
|
||||||
|
def parameters
|
||||||
|
JSON.parse(parameters_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parameters=(parameters_hash)
|
||||||
|
self.parameters_json = parameters_hash.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_dsl_setup(setup)
|
||||||
|
self.set(
|
||||||
|
name: setup.name,
|
||||||
|
success_name: setup.success.name,
|
||||||
|
parameters_json: setup.parameters.to_json,
|
||||||
|
icon: setup.icon,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: use a singleton instead of an include
|
||||||
|
AVAILABLE_SUCCESS = LifePex::AchievementDSL::Includer::SUCCESS_INDEX.keys
|
||||||
|
def to_dsl_setup
|
||||||
|
current = self
|
||||||
|
current_parameters = self.parameters()
|
||||||
|
LifePex::AchievementDSL::Includer::SUCCESS_INDEX[current[:success_name]].setup(current[:name], to: current[:user_id]) do
|
||||||
|
icon current[:icon].to_s
|
||||||
|
current_parameters.each do |name, value|
|
||||||
|
parameter name => value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,25 +1,21 @@
|
||||||
class LifePex::Pex < Sequel::Model(:pexs)
|
class LifePex::Pex < Sequel::Model(LifePex::DB[:pexs].order(:category, :name))
|
||||||
one_to_many :user_pexs
|
one_to_many :user_pexs
|
||||||
many_to_one :user
|
many_to_one :user
|
||||||
|
one_to_many :recalls
|
||||||
|
|
||||||
|
# note: wont work on #update
|
||||||
|
def before_validation
|
||||||
|
self.category = self.category.to_s.downcase
|
||||||
|
self.hidden = false if self.hidden.nil?
|
||||||
|
end
|
||||||
|
|
||||||
# common interface for setup_user_pexs_*
|
# common interface for setup_user_pexs_*
|
||||||
def self.setup_user_pexs(user_id: nil, user_pexs: nil)
|
def self.setup_user_pexs(user_id: nil, user_pexs: nil)
|
||||||
if user_id
|
raise RuntimeError.new "user_id is required" if user_id.nil?
|
||||||
setup_user_pexs_by_user_id(user_id)
|
user_pexs = LifePex::UserPex.where(user_id: user_id) if user_pexs.nil?
|
||||||
elsif user_pexs
|
pexs = LifePex::Pex
|
||||||
setup_user_pexs_by_user_pexs(user_pexs)
|
.where(user_id: user_id)
|
||||||
else
|
.order(:name).all()
|
||||||
raise "Invalid setup"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.setup_user_pexs_by_user_id(user_id)
|
|
||||||
user_pexs = LifePex::UserPex.where(user_id: user_id)
|
|
||||||
setup_user_pexs_by_user_pexs(user_pexs)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.setup_user_pexs_by_user_pexs(user_pexs)
|
|
||||||
pexs = LifePex::Pex.order(:name).all()
|
|
||||||
.group_by { |pex| pex[:id] }
|
.group_by { |pex| pex[:id] }
|
||||||
.map { |id, pex_group| [id, pex_group[0]] }
|
.map { |id, pex_group| [id, pex_group[0]] }
|
||||||
.to_h
|
.to_h
|
||||||
|
@ -72,4 +68,8 @@ class LifePex::Pex < Sequel::Model(:pexs)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bookmarked?
|
||||||
|
self.flag == "bookmarked"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
4
src/models/recall.rb
Normal file
4
src/models/recall.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class LifePex::Recall < Sequel::Model(LifePex::DB[:recalls].order(:name))
|
||||||
|
many_to_one :user
|
||||||
|
many_to_one :pex
|
||||||
|
end
|
|
@ -1,4 +1,23 @@
|
||||||
class LifePex::User < Sequel::Model(:users)
|
class LifePex::User < Sequel::Model(:users)
|
||||||
one_to_many :user_pexs
|
one_to_many :user_pexs
|
||||||
one_to_many :pexs, order: %i(category name)
|
one_to_many :pexs
|
||||||
|
one_to_many :recalls
|
||||||
|
|
||||||
|
def password=(clear_password)
|
||||||
|
self.hashed_password = BCrypt::Password.create(clear_password)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recalls_not_validated(cached: true)
|
||||||
|
@recalls_not_validated = nil if !cached
|
||||||
|
|
||||||
|
pexs = LifePex::Pex.setup_user_pexs(user_id: id, user_pexs: user_pexs)
|
||||||
|
|
||||||
|
@recalls_not_validated ||= recalls.filter do |recall|
|
||||||
|
pex = pexs[recall[:pex_id]]
|
||||||
|
validated = pex[:count_by_date].filter do |date, _|
|
||||||
|
date >= Date.today - recall[:span_duration]
|
||||||
|
end.values.sum
|
||||||
|
validated < recall[:repeated]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
class LifePex::UserPex < Sequel::Model(:user_pexs)
|
class LifePex::UserPex < Sequel::Model(:user_pexs)
|
||||||
many_to_one :user
|
many_to_one :user
|
||||||
many_to_one :pex
|
many_to_one :pex
|
||||||
|
|
||||||
|
def self.last_inserted_at(user_id)
|
||||||
|
LifePex::UserPex
|
||||||
|
# .join(:pexs, :id => :pex_id)
|
||||||
|
# .where(Sequel[:user_pexs][:user_id] => user_id)
|
||||||
|
# .group(:pex_id)
|
||||||
|
# .select_append{max(created_at).as(:last_inserted_at)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
47
src/systems/achievement.rb
Normal file
47
src/systems/achievement.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
|
||||||
|
class LifePex::Systems::AchievementSystem < LifePex::Systems::AuthSystem
|
||||||
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
|
get "/achievements", auth: [] do
|
||||||
|
achievements = LifePex::Achievement.where(user_id: current_user_id).all
|
||||||
|
slim :achievements, locals: { achievements: achievements }
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/achievements/new", auth: [] do
|
||||||
|
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
||||||
|
slim :achievement_form, locals: { success: success }
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/achievements", auth: [] do
|
||||||
|
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
||||||
|
begin
|
||||||
|
parameters_json = success.format_parameters_hash(params["inputParams"]).to_json
|
||||||
|
achievement = LifePex::Achievement.new(
|
||||||
|
user_id: current_user_id,
|
||||||
|
success_name: params["successName"],
|
||||||
|
name: params["inputName"],
|
||||||
|
icon: params["inputMedal"],
|
||||||
|
parameters_json: parameters_json,
|
||||||
|
).save
|
||||||
|
achievements = LifePex::Achievement.where(user_id: current_user_id).all
|
||||||
|
slim :achievements, locals: { achievements: achievements, flash: { success: "Added achievements '#{achievement.name}" } }
|
||||||
|
rescue => err
|
||||||
|
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
||||||
|
slim :achievement_form, locals: { success: success, flash: { danger: err.message } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/achievements/delete", auth: [] do
|
||||||
|
amount = LifePex::Achievement.where(user_id: current_user_id, id: params['id']).destroy
|
||||||
|
achievements = LifePex::Achievement.where(user_id: current_user_id).all
|
||||||
|
slim :achievements, locals: { achievements: achievements, flash: { success: "Removed #{amount} achievements" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
# include JSON::API
|
||||||
|
|
||||||
|
# extend DocMyRoutes::Annotatable
|
||||||
|
# register Sinatra::Namespace
|
||||||
|
# include LifePex::Systems::ApiList
|
||||||
|
end
|
22
src/systems/api_response.rb
Normal file
22
src/systems/api_response.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module LifePex::Systems::ApiResponse
|
||||||
|
def api_response(any)
|
||||||
|
content_type 'application/json'
|
||||||
|
any.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def api_error(status = 500, message = "Internal error", **more)
|
||||||
|
halt(status, {
|
||||||
|
message => message,
|
||||||
|
**more,
|
||||||
|
}.to_json)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,33 @@
|
||||||
class LifePex::Systems::AuthSystem < Sinatra::Base
|
class LifePex::Systems::AuthSystem < Sinatra::Base
|
||||||
helpers Sinatra::Cookies
|
helpers Sinatra::Cookies
|
||||||
include JSON::API
|
include JSON::API
|
||||||
|
include LifePex::UsersHelper
|
||||||
|
|
||||||
|
def setup_user_cookie!(user_id)
|
||||||
|
response.set_cookie(
|
||||||
|
"auth",
|
||||||
|
{
|
||||||
|
value: JWT.encode({ "user_id" => user_id }, LifePex::SECRET),
|
||||||
|
expires: Time.now + 2.days,
|
||||||
|
path: "/",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def renew_user_cookie!
|
||||||
|
response.set_cookie(
|
||||||
|
"auth",
|
||||||
|
{
|
||||||
|
value: cookies["auth"],
|
||||||
|
expires: Time.now + 2.days,
|
||||||
|
path: "/",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def user_id_decoded(cookies = nil)
|
def user_id_decoded(cookies = nil)
|
||||||
cookies = cookies() if cookies.nil?
|
cookies = cookies() if cookies.nil?
|
||||||
begin
|
begin
|
||||||
decoded = JWT.decode(cookies["auth"], LifePex::SECRET)
|
decoded = JWT.decode(cookies["auth"], LifePex::SECRET)
|
||||||
|
renew_user_cookie!
|
||||||
decoded[0]["user_id"]
|
decoded[0]["user_id"]
|
||||||
rescue => err
|
rescue => err
|
||||||
STDERR.puts "user_id_decoded: #{err}"
|
STDERR.puts "user_id_decoded: #{err}"
|
||||||
|
@ -37,7 +59,7 @@ class LifePex::Systems::AuthSystem < Sinatra::Base
|
||||||
condition do
|
condition do
|
||||||
unless logged_in?
|
unless logged_in?
|
||||||
if accept_json?
|
if accept_json?
|
||||||
halt 303, { message: 'You need to /api/v1/register an account and /api/v1/login to get a cookie first' }.to_json
|
halt 401, { message: 'You need to POST /api/user/v1/register to register an account and POST /api/user/v1/login to get a cookie first' }.to_json
|
||||||
else
|
else
|
||||||
redirect "/login", 303
|
redirect "/login", 303
|
||||||
end
|
end
|
||||||
|
|
13
src/systems/csrf.rb
Normal file
13
src/systems/csrf.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module LifePex::Systems::CrlfHelper
|
||||||
|
def self.included(m)
|
||||||
|
m.helpers do
|
||||||
|
def csrf_token
|
||||||
|
Rack::Csrf.csrf_token(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def csrf_tag
|
||||||
|
Rack::Csrf.csrf_tag(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,49 +1,38 @@
|
||||||
require "date"
|
require "date"
|
||||||
|
require "active_support/all"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
require_relative "../utils/users.rb"
|
||||||
|
|
||||||
class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
include JSON::API
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
def my_user_pexs cookies, date = nil
|
|
||||||
params = {
|
|
||||||
user_id: user_id_decoded(cookies),
|
|
||||||
}
|
|
||||||
params[:created_at] = date unless date.nil?
|
|
||||||
LifePex::UserPex.where(params).all()
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_user_date
|
|
||||||
date = cookies["date"]
|
|
||||||
if date && Date.respond_to?(date)
|
|
||||||
Date.send date
|
|
||||||
elsif date == "yesterday"
|
|
||||||
Date.today - 1
|
|
||||||
else
|
|
||||||
Date.today
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/today", auth: [] do
|
get "/today", auth: [] do
|
||||||
cookies["date"] = "today"
|
cookies.set "date", { value: "now", 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
|
||||||
|
|
||||||
def pex_by_models(params)
|
def pex_by_models(params, filter_hidden: true)
|
||||||
user_pexs = my_user_pexs(cookies, get_user_date)
|
user_pexs = my_user_pexs(cookies, get_user_date)
|
||||||
user_pexs_amounts = user_pexs.group_by { |user_pex| user_pex[:pex_id] }
|
user_pexs_amounts = user_pexs.group_by { |user_pex| user_pex[:pex_id] }
|
||||||
pexs = current_user.pexs.to_a
|
pexs = LifePex::Pex.where(user_id: current_user_id)
|
||||||
|
|
||||||
|
pexs = pexs.where(hidden: false) if filter_hidden
|
||||||
|
pexs = pexs.all
|
||||||
|
|
||||||
|
user_pexs_last_inserted_at = LifePex::UserPex.last_inserted_at(current_user_id).all
|
||||||
|
|
||||||
pex_by_models = pexs.map do |pex|
|
pex_by_models = pexs.map do |pex|
|
||||||
{
|
{
|
||||||
id: pex[:id],
|
pex: pex,
|
||||||
name: pex[:name],
|
|
||||||
amount: pex[:amount],
|
|
||||||
category: pex[:category],
|
|
||||||
user_pexs: {
|
user_pexs: {
|
||||||
amount: (user_pexs_amounts[pex[:id]] || []).length
|
at_date: (user_pexs_amounts[pex[:id]] || []).length,
|
||||||
|
last_inserted_at: (user_pexs_last_inserted_at.find{ |up| up[:pex_id] == pex[:id] } || {})[:last_inserted_at],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -51,7 +40,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/", auth: [], provides: 'html' do
|
get "/", auth: [], provides: 'html' do
|
||||||
pex_by_models = pex_by_models(params)
|
pex_by_models = pex_by_models(params, filter_hidden: params['filter_hidden'] != 'false')
|
||||||
slim :index, locals: { pex_by_models: pex_by_models }
|
slim :index, locals: { pex_by_models: pex_by_models }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,28 +48,39 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
user_id = user_id_decoded(cookies)
|
user_id = user_id_decoded(cookies)
|
||||||
pex_id = params["id"]
|
pex_id = params["id"]
|
||||||
if params["type"] == "+"
|
if params["type"] == "+"
|
||||||
LifePex::UserPex.insert(user_id: user_id, pex_id: pex_id, created_at: get_user_date)
|
LifePex::UserPex.new(user_id: user_id, pex_id: pex_id, created_at: get_user_date).save
|
||||||
elsif params["type"] == "-"
|
elsif params["type"] == "-"
|
||||||
id = LifePex::UserPex.where(user_id: user_id, pex_id: pex_id, created_at: get_user_date).select(:id).first[:id]
|
user_pex = LifePex::UserPex.where(user_id: user_id, pex_id: pex_id, created_at: get_user_date).select(:id).first
|
||||||
LifePex::UserPex.where(id: id).delete
|
halt 400, "No user pex can be found" if user_pex.nil?
|
||||||
|
LifePex::UserPex.where(id: user_pex[:id]).delete
|
||||||
end
|
end
|
||||||
redirect "/"
|
redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/pexs", auth: [] do
|
get "/pexs", auth: [] do
|
||||||
slim :pex_form
|
categories = LifePex::Pex
|
||||||
|
.select(:category)
|
||||||
|
.where(user_id: current_user_id)
|
||||||
|
.distinct
|
||||||
|
.pluck(:category)
|
||||||
|
slim :pex_form, locals: { categories: categories }
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/pexs/update", auth: [] do
|
get "/pexs/update", auth: [] do
|
||||||
pex = LifePex::Pex.where(id: params["id"]).first
|
pex = LifePex::Pex.where(id: params["id"]).first
|
||||||
slim :pex_form, locals: { pex: pex, submit_name: "update" }
|
categories = LifePex::Pex
|
||||||
|
.select(:category)
|
||||||
|
.where(user_id: current_user_id)
|
||||||
|
.distinct
|
||||||
|
.pluck(:category)
|
||||||
|
slim :pex_form, locals: { pex: pex, categories: categories, submit_name: "update" }
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/pexs", auth: [] do
|
post "/pexs", auth: [] do
|
||||||
pex_id = if params["id"]
|
pex_id = if params["id"]
|
||||||
LifePex::Pex.where(id: params["id"]).update(name: params['name'], category: params['category'], amount: params['amount'])
|
LifePex::Pex.where(id: params["id"]).update(name: params['name'], category: params['category'].to_s.downcase, amount: params['amount'])
|
||||||
else
|
else
|
||||||
LifePex::Pex.insert(name: params['name'], category: params['category'], amount: params['amount'], user_id: current_user.id)
|
LifePex::Pex.new(name: params['name'], category: params['category'], amount: params['amount'], user_id: current_user.id).save
|
||||||
end
|
end
|
||||||
if pex_id
|
if pex_id
|
||||||
redirect "/"
|
redirect "/"
|
||||||
|
@ -95,6 +95,19 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
slim :pex_removed, locals: { flash: { success: "#{removed} model and #{links_removed} marks removed" } }
|
slim :pex_removed, locals: { flash: { success: "#{removed} model and #{links_removed} marks removed" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
post "/pexs/hide", auth: [] do
|
||||||
|
pex = LifePex::Pex.find(id: params['id'])
|
||||||
|
pex.set(hidden: !pex.hidden).save
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/pexs/bookmark", auth: [] do
|
||||||
|
pex = LifePex::Pex.find(id: params['id'])
|
||||||
|
set_flag = pex.bookmarked? ? "" : "bookmarked"
|
||||||
|
pex.set(flag: set_flag).save
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
def compute_highchart_cumul(pex_data)
|
def compute_highchart_cumul(pex_data)
|
||||||
total = 0
|
total = 0
|
||||||
pex_data.map do |v|
|
pex_data.map do |v|
|
||||||
|
@ -102,19 +115,28 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_recap_infos(cookies)
|
def get_recap_infos(params, cookies)
|
||||||
user_pexs = my_user_pexs(cookies)
|
user_pexs = my_user_pexs(cookies)
|
||||||
pexs = LifePex::Pex.setup_user_pexs(user_pexs: user_pexs)
|
pexs = LifePex::Pex.setup_user_pexs(user_id: current_user_id, user_pexs: user_pexs)
|
||||||
total_xp = pexs.values.sum { |up| up[:total_by_date].values.sum }
|
total_xp = pexs.values.sum { |up| up[:total_by_date].values.sum }
|
||||||
level = LifePex::Level.new total_xp
|
level = LifePex::Level.new total_xp
|
||||||
all_dates = user_pexs.map(&:created_at).uniq
|
|
||||||
all_category = pexs.values.map(&:category).uniq
|
all_category = pexs.values.map(&:category).uniq
|
||||||
|
|
||||||
|
start_date = 60.days.ago.to_date
|
||||||
|
if params["days_ago"]
|
||||||
|
start_date = params["days_ago"].to_i.days.ago.to_date
|
||||||
|
elsif params["since_first_jan"]
|
||||||
|
year = Date.today.year
|
||||||
|
first_jan = Date.new(year, 1, 1)
|
||||||
|
start_date = first_jan
|
||||||
|
end
|
||||||
|
all_dates = (start_date..(Date.today)).to_a
|
||||||
|
|
||||||
pex_table_by_date = all_dates.map { |date| [date, 0] }.to_h
|
pex_table_by_date = all_dates.map { |date| [date, 0] }.to_h
|
||||||
pexs.values.each do |pex|
|
pexs.values.each do |pex|
|
||||||
pex[:total_by_date].each do |date, total|
|
pex[:total_by_date].each do |date, total|
|
||||||
# pex_table_by_date[date] ||= 0
|
# pex_table_by_date[date] ||= 0
|
||||||
pex_table_by_date[date] += total
|
pex_table_by_date[date] += total if pex_table_by_date[date]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -130,7 +152,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
category = pex[:category]
|
category = pex[:category]
|
||||||
# pex_table_by_category_and_date[category] ||= {}
|
# pex_table_by_category_and_date[category] ||= {}
|
||||||
# pex_table_by_category_and_date[category][date] ||= 0
|
# pex_table_by_category_and_date[category][date] ||= 0
|
||||||
pex_table_by_category_and_date[category][date] += total
|
pex_table_by_category_and_date[category][date] += total if pex_table_by_category_and_date[category][date]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -145,14 +167,32 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
}
|
}
|
||||||
end.unshift(highchart_total)
|
end.unshift(highchart_total)
|
||||||
|
|
||||||
|
recalls = LifePex::Recall.where(user_id: current_user_id).all
|
||||||
|
recalls_not_validated = recalls.filter do |recall|
|
||||||
|
pex = pexs[recall[:pex_id]]
|
||||||
|
validated = pex[:count_by_date].filter do |date, _|
|
||||||
|
date >= Date.today - recall[:span_duration]
|
||||||
|
end.values.sum
|
||||||
|
validated < recall[:repeated]
|
||||||
|
end
|
||||||
|
|
||||||
|
all_setup = LifePex::Achievement.where(user_id: current_user_id).map(&:to_dsl_setup)
|
||||||
|
medals = all_setup.filter do |success|
|
||||||
|
success.call(user_pexs, pexs.values)
|
||||||
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
pex_tables: highchart_tables,
|
pex_tables: highchart_tables,
|
||||||
|
start_date: start_date,
|
||||||
|
all_dates: all_dates,
|
||||||
level: level,
|
level: level,
|
||||||
|
medals: medals,
|
||||||
|
recalls_not_validated: recalls_not_validated,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/recap", auth: [], provides: 'html' do
|
get "/recap", auth: [], provides: 'html' do
|
||||||
recap_infos = get_recap_infos(cookies)
|
recap_infos = get_recap_infos(params, cookies)
|
||||||
slim :recap, locals: recap_infos
|
slim :recap, locals: recap_infos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -163,9 +203,10 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
get "/recap", auth: [], provides: 'json' do
|
get "/recap", auth: [], provides: 'json' do
|
||||||
recap_infos = get_recap_infos(cookies)
|
recap_infos = get_recap_infos(params, cookies)
|
||||||
{
|
{
|
||||||
pex_tables: recap_infos[:pex_tables],
|
pex_tables: recap_infos[:pex_tables],
|
||||||
|
all_dates: recap_infos[:all_dates],
|
||||||
level: recap_infos[:level].to_h,
|
level: recap_infos[:level].to_h,
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
@ -194,12 +235,12 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
parameter :amount, required: true, type: 'number', in: 'body'
|
parameter :amount, required: true, type: 'number', in: 'body'
|
||||||
parameter :auth, required: true, type: 'string', in: 'cookies'
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
post "/", auth: [], provides: 'json' do
|
post "/", auth: [], provides: 'json' do
|
||||||
pex_id = LifePex::Pex.insert(
|
pex_id = LifePex::Pex.new(
|
||||||
name: json_params['name'],
|
name: json_params['name'],
|
||||||
category: json_params['category'],
|
category: json_params['category'],
|
||||||
amount: json_params['amount'],
|
amount: json_params['amount'],
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
)
|
).save
|
||||||
{
|
{
|
||||||
message: "entity created",
|
message: "entity created",
|
||||||
pex: LifePex::Pex.find(id: pex_id),
|
pex: LifePex::Pex.find(id: pex_id),
|
||||||
|
@ -231,7 +272,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
post "/by_id/:id", auth: [], provides: 'json' do
|
post "/by_id/:id", auth: [], provides: 'json' do
|
||||||
LifePex::Pex.where(id: params["id"]).update(
|
LifePex::Pex.where(id: params["id"]).update(
|
||||||
name: json_params['name'],
|
name: json_params['name'],
|
||||||
category: json_params['category'],
|
category: json_params['category'].to_s.downcase,
|
||||||
amount: json_params['amount'],
|
amount: json_params['amount'],
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
@ -266,7 +307,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
post "/by_id/:id/mark", auth: [], provides: 'json' do
|
post "/by_id/:id/mark", auth: [], provides: 'json' do
|
||||||
user_id = user_id_decoded
|
user_id = user_id_decoded
|
||||||
pex_id = params["id"]
|
pex_id = params["id"]
|
||||||
LifePex::UserPex.insert(user_id: user_id, pex_id: pex_id, created_at: get_user_date)
|
LifePex::UserPex.new(user_id: user_id, pex_id: pex_id, created_at: get_user_date).save
|
||||||
{ message: "ok" }.to_json
|
{ message: "ok" }.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ require_relative "./pex.rb"
|
||||||
|
|
||||||
class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
# include JSON::API # included by PexSystem
|
# include JSON::API # included by PexSystem
|
||||||
|
include LifePex::Systems::ApiResponse
|
||||||
|
|
||||||
# extend DocMyRoutes::Annotatable # included by PexSystem
|
# extend DocMyRoutes::Annotatable # included by PexSystem
|
||||||
# register Sinatra::Namespace # included by PexSystem
|
# register Sinatra::Namespace # included by PexSystem
|
||||||
|
@ -24,7 +25,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :pluck, required: false, type: 'string', in: 'query', description: 'improve performance by only fetching one field'
|
parameter :pluck, required: false, type: 'string', in: 'query', description: 'improve performance by only fetching one field'
|
||||||
get "", auth: [], provides: 'json' do
|
get '', auth: [], provides: "json" do
|
||||||
pexs = current_user.pexs
|
pexs = current_user.pexs
|
||||||
pexs = pexs.select(json_params["pluck"]) if json_params["pluck"]
|
pexs = pexs.select(json_params["pluck"]) if json_params["pluck"]
|
||||||
pexs.to_json
|
pexs.to_json
|
||||||
|
@ -37,10 +38,15 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
parameter :category, required: true, type: 'string', in: 'body'
|
parameter :category, required: true, type: 'string', in: 'body'
|
||||||
parameter :amount, required: true, type: 'number', in: 'body'
|
parameter :amount, required: true, type: 'number', in: 'body'
|
||||||
parameter :auth, required: true, type: 'string', in: 'cookies'
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
post "/", auth: [], provides: 'json' do
|
post '', auth: [], provides: 'json' do
|
||||||
raise RunetimeError.new "\"name\" is required" unless json_params["name"]
|
halt 400, "\"name\" is required" unless json_params["name"]
|
||||||
raise RunetimeError.new "\"category\" is required" unless json_params["category"]
|
halt 400, "\"category\" is required" unless json_params["category"]
|
||||||
raise RunetimeError.new "\"amount\" is required" unless json_params["amount"]
|
halt 400, "\"amount\" is required" unless json_params["amount"]
|
||||||
|
halt 409, "conflict with existing pex with the same characteristics" if LifePex::Pex.find(
|
||||||
|
name: json_params['name'],
|
||||||
|
category: json_params['category'],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
pex_id = LifePex::Pex.insert(
|
pex_id = LifePex::Pex.insert(
|
||||||
name: json_params['name'],
|
name: json_params['name'],
|
||||||
category: json_params['category'],
|
category: json_params['category'],
|
||||||
|
@ -53,54 +59,148 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
summary 'Update an existing pex'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, require: true, type: 'integer', in: 'path'
|
||||||
|
parameter :name, required: false, type: 'string', in: 'body'
|
||||||
|
parameter :category, required: false, type: 'string', in: 'body'
|
||||||
|
parameter :amount, required: false, type: 'number', in: 'body'
|
||||||
|
parameter :auth, required: true, ype: 'string', in: 'cookies'
|
||||||
|
put '/:id', auth: [], provides: 'json' do
|
||||||
|
pex = LifePex::Pex.find(
|
||||||
|
id: params["id"],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
pex.name = json_params['name'] unless json_params['name'].nil?
|
||||||
|
pex.category = json_params['category'] unless json_params['category'].nil?
|
||||||
|
pex.amount = json_params['amount'] unless json_params['amount'].nil?
|
||||||
|
pex.save
|
||||||
|
{
|
||||||
|
message: "entity updated",
|
||||||
|
pex: pex,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
summary 'Delete an existing pex'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, require: true, type: 'integer', in: 'path'
|
||||||
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
|
delete '/:id', auth: [], provides: 'json' do
|
||||||
|
pex = LifePex::Pex.find(
|
||||||
|
id: params["id"],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
pex.destroy
|
||||||
|
{
|
||||||
|
message: "entity deleted",
|
||||||
|
pex: pex,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
summary 'Show the state of a pex'
|
summary 'Show the state of a pex'
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'string', in: 'path'
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
get "/:id", auth: [], provides: 'json' do
|
get '/:id', auth: [], provides: 'json' do
|
||||||
{
|
pex = LifePex::Pex.find(
|
||||||
pex: LifePex::Pex.find(
|
|
||||||
id: params["id"],
|
id: params["id"],
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
),
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
{
|
||||||
|
pex: pex,
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Show the state of a pex and load advanced infos about it'
|
summary 'Show the state of a pex and load advanced infos about it'
|
||||||
produces 'application/json'
|
produces 'application/json,text/html'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'string', in: 'path'
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
get "/:id/more", auth: [], provides: 'json' do
|
get '/:id/more', auth: [], provides: 'json' do
|
||||||
pex = LifePex::Pex.find(
|
pex = LifePex::Pex.find(
|
||||||
id: params["id"],
|
id: params["id"],
|
||||||
user_id: current_user_id,
|
user_id: current_user_id,
|
||||||
)
|
)
|
||||||
{
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
user_pexs = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex.id
|
||||||
|
).select(:created_at).all
|
||||||
|
date = date_input_convertor(json_params["date"])
|
||||||
|
user_pexs_by_date = user_pexs.group_by(&:created_at).transform_values { |group| group.size }
|
||||||
|
user_pexs_at_date = user_pexs_by_date[date]
|
||||||
|
|
||||||
|
pex_with_more = {
|
||||||
pex: pex,
|
pex: pex,
|
||||||
user_pexs: LifePex::UserPex.where(user_id: current_user_id, pex_id: pex.id).select(:created_at).all.group_by(&:created_at).transform_values { |group| group.size }
|
user_pexs: {
|
||||||
}.to_json
|
by_date: user_pexs_by_date,
|
||||||
|
at_date: user_pexs_at_date,
|
||||||
|
total: user_pexs.size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
given_content_type do
|
||||||
|
json pex_with_more.to_json
|
||||||
|
html slim_partial("pex_row", locals: { pex_with_amount: pex_with_more })
|
||||||
|
default do
|
||||||
|
api_error(400, message: "No valid accepted content found.")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Get the amount of a pex each day since first occurence since 1 january'
|
summary 'Get the amount of a pex each day since first occurence since a given date'
|
||||||
notes ""
|
notes ''
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'integer', in: 'path'
|
parameter :id, required: true, type: 'integer', in: 'path'
|
||||||
parameter :auth, required: true, type: 'string', in: 'cookies'
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
post "/:id/recap", auth: [], provides: 'json' do
|
parameter :days_ago, required: false, type: 'integer', in: 'body', description: "default: 30, limit the amount of results to the n last days included"
|
||||||
user_id = user_id_decoded
|
parameter :since_first_jan, required: false, type: 'boolean', in: 'body', description: "default: false, if no days_ago defined, limit to all dates after (included) 1st jan of the current year"
|
||||||
|
post '/:id/recap', auth: [], provides: 'json' do
|
||||||
pex_id = params["id"]
|
pex_id = params["id"]
|
||||||
|
pex = LifePex::Pex.find(id: pex_id, user_id: current_user_id)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
|
||||||
|
start_date = 30.days.ago
|
||||||
|
if params["days_ago"]
|
||||||
|
start_date = params["days_ago"].to_i.days.ago
|
||||||
|
elsif params["since_first_jan"]
|
||||||
year = Date.today.year
|
year = Date.today.year
|
||||||
first_jan = Date.new(year, 1, 1)
|
first_jan = Date.new(year, 1, 1)
|
||||||
pex = LifePex::Pex.find(id: pex_id)
|
start_date = first_jan
|
||||||
user_pexs = LifePex::UserPex.where {
|
end
|
||||||
self.user_id == user_id && self.pex_id == pex_id && created_at > first_jan
|
|
||||||
}.first
|
user_pexs = LifePex::UserPex.where { # TODO query that
|
||||||
|
self.user_id == current_user_id && self.pex_id == pex_id && created_at >= start_date
|
||||||
|
}
|
||||||
{ pex: pex, user_pexs: user_pexs }.to_json
|
{ pex: pex, user_pexs: user_pexs }.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace '/infos' do
|
||||||
|
|
||||||
|
summary 'Get for all existing pex the last date a user_pex has been added'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
|
get '/last-inserts', auth: [], provides: 'json' do
|
||||||
|
pexs = LifePex::UserPex.last_inserted_at(current_user.id)
|
||||||
|
pexs.map { |pex|
|
||||||
|
{
|
||||||
|
pex_id: pex[:pex_id],
|
||||||
|
name: pex[:name],
|
||||||
|
last_inserted_at: pex[:last_inserted_at],
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
include LifePex::Systems::ApiList
|
include LifePex::Systems::ApiList
|
||||||
end
|
end
|
||||||
|
|
47
src/systems/recall.rb
Normal file
47
src/systems/recall.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
|
||||||
|
class LifePex::Systems::RecallSystem < LifePex::Systems::AuthSystem
|
||||||
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
|
get "/recalls", auth: [] do
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls }
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/recalls/new", auth: [] do
|
||||||
|
pexs = current_user.pexs
|
||||||
|
slim :recall_form, locals: { pexs: pexs }
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/recalls", auth: [] do
|
||||||
|
flash = {}
|
||||||
|
if !LifePex::Pex.where(user_id: current_user_id, id: params["pex_id"]).first
|
||||||
|
flash[:danger] = "Invalid pex"
|
||||||
|
else
|
||||||
|
recall = LifePex::Recall.new(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: params["pex_id"],
|
||||||
|
name: params["name"],
|
||||||
|
span_duration: params["span_duration"],
|
||||||
|
repeated: params["repeated"],
|
||||||
|
).save
|
||||||
|
flash[:success] = "Recall #{recall.name} created"
|
||||||
|
end
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls, flash: flash }
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/recalls/delete", auth: [] do
|
||||||
|
recall = LifePex::Recall.where(user_id: current_user_id, id: params["id"]).first
|
||||||
|
flash = {}
|
||||||
|
if recall
|
||||||
|
recall.destroy
|
||||||
|
flash[:success] = "Recall #{recall.name} removed"
|
||||||
|
else
|
||||||
|
flash[:danger] = "Recall do not exists"
|
||||||
|
end
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls, flash: flash }
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,9 @@
|
||||||
require "yaml"
|
require "yaml"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
|
||||||
class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
include JSON::API
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
DEFAULT_PEXS_FOR_NEW_USERS = YAML.load_file "config/default_pexs_for_new_users.yaml"
|
DEFAULT_PEXS_FOR_NEW_USERS = YAML.load_file "config/default_pexs_for_new_users.yaml"
|
||||||
|
|
||||||
|
@ -12,7 +14,7 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
def login(params)
|
def login(params)
|
||||||
user = LifePex::User.where(username: params["username"]).first
|
user = LifePex::User.where(username: params["username"]).first
|
||||||
if user && BCrypt::Password.new(user[:hashed_password]) == params["password"]
|
if user && BCrypt::Password.new(user[:hashed_password]) == params["password"]
|
||||||
cookies["auth"] = JWT.encode({ "user_id" => user[:id] }, LifePex::SECRET)
|
setup_user_cookie!(user[:id])
|
||||||
user
|
user
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
|
@ -21,7 +23,6 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
|
|
||||||
post "/login", provides: 'html' do
|
post "/login", provides: 'html' do
|
||||||
if user = login(params)
|
if user = login(params)
|
||||||
cookies["auth"] = JWT.encode({ "user_id" => user[:id] }, LifePex::SECRET)
|
|
||||||
redirect "/"
|
redirect "/"
|
||||||
else
|
else
|
||||||
slim :login, locals: { flash: { danger: "Failed to login" } }
|
slim :login, locals: { flash: { danger: "Failed to login" } }
|
||||||
|
@ -36,12 +37,12 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
user = LifePex::User.where(username: params["username"]).first
|
user = LifePex::User.where(username: params["username"]).first
|
||||||
if !user
|
if !user
|
||||||
hashed_password = BCrypt::Password.create params["password"]
|
hashed_password = BCrypt::Password.create params["password"]
|
||||||
user_id = LifePex::User.insert(username: params["username"], hashed_password: hashed_password)
|
user = LifePex::User.new(username: params["username"], hashed_password: hashed_password).save
|
||||||
cookies["auth"] = JWT.encode({ "user_id" => user_id }, LifePex::SECRET)
|
cookies["auth"] = JWT.encode({ "user_id" => user.id }, LifePex::SECRET)
|
||||||
DEFAULT_PEXS_FOR_NEW_USERS.each do |pex|
|
DEFAULT_PEXS_FOR_NEW_USERS.each do |pex|
|
||||||
LifePex::Pex.insert(**pex, user_id: user_id)
|
LifePex::Pex.new(**pex, user_id: user.id).save
|
||||||
end
|
end
|
||||||
user_id
|
user.id
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -55,7 +56,7 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/logout", auth: [] do
|
post "/logout", auth: [] do
|
||||||
cookies.delete "auth"
|
cookies.delete "auth"
|
||||||
slim :logout, locals: { flash: { success: "Logged out" } }
|
slim :logout, locals: { flash: { success: "Logged out" } }
|
||||||
end
|
end
|
||||||
|
@ -66,7 +67,7 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
|
|
||||||
def change_password(params)
|
def change_password(params)
|
||||||
hashed_password = BCrypt::Password.create params["password"]
|
hashed_password = BCrypt::Password.create params["password"]
|
||||||
current_user.update(hashed_password: hashed_password)
|
LifePex::User.where(id: current_user_id).update(hashed_password: hashed_password)
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/password", auth: [], provides: 'html' do
|
post "/password", auth: [], provides: 'html' do
|
||||||
|
@ -82,6 +83,40 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
slim :about
|
slim :about
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/preferences" do
|
||||||
|
show_preferences
|
||||||
|
end
|
||||||
|
|
||||||
|
UserPreferenceCookie = Struct.new(:cookie, :allow_blank, :convert, :html, :description)
|
||||||
|
USER_PREFERENCES = {
|
||||||
|
"inputRecapDays" => UserPreferenceCookie.new("recap_days", false, :to_i, { type: "text" }, "Amount of days to show in the recap"),
|
||||||
|
"inputLateDayOffset" => UserPreferenceCookie.new("late_day_offset", false, :to_f, { type: "number", min: "0", max: "24", step: "0.1" }, "Offset for today (so a few hours after midnight is still today)"),
|
||||||
|
"inputShowFullDate" => UserPreferenceCookie.new("show_full_date", true, :to_s, { type: "checkbox" }, "Show or hide the date of the current tab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
post "/preferences" do
|
||||||
|
USER_PREFERENCES.each do |param_name, upc|
|
||||||
|
current_param = params[param_name]
|
||||||
|
next if !upc.allow_blank && current_param.blank?
|
||||||
|
|
||||||
|
cookies[upc.cookie] =
|
||||||
|
case upc.convert
|
||||||
|
when Symbol
|
||||||
|
current_param.send(upc.convert)
|
||||||
|
when Proc
|
||||||
|
upc.convert.call(current_param)
|
||||||
|
else
|
||||||
|
current_param
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
show_preferences
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_preferences
|
||||||
|
slim :preferences
|
||||||
|
end
|
||||||
|
|
||||||
extend DocMyRoutes::Annotatable
|
extend DocMyRoutes::Annotatable
|
||||||
register Sinatra::Namespace
|
register Sinatra::Namespace
|
||||||
namespace '/api/user/v1' do
|
namespace '/api/user/v1' do
|
||||||
|
|
138
src/systems/user_pex.rb
Normal file
138
src/systems/user_pex.rb
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
require_relative "./api_response"
|
||||||
|
require "csv"
|
||||||
|
|
||||||
|
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
||||||
|
include LifePex::Systems::ApiResponse
|
||||||
|
set :protection, :except => [:frame_options, :json_csrf]
|
||||||
|
|
||||||
|
extend DocMyRoutes::Annotatable # included by PexSystem
|
||||||
|
register Sinatra::Namespace # included by PexSystem
|
||||||
|
namespace "/api/user-pex/v1" do
|
||||||
|
|
||||||
|
summary "Export every single user pex and pex"
|
||||||
|
produces "application/json,application/csv"
|
||||||
|
status_codes [200]
|
||||||
|
# parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
# parameter :date, required: true, type: "string", in: "path"
|
||||||
|
get "/export", auth: [], provides: %w(json application/csv) do
|
||||||
|
if accept? "application/csv"
|
||||||
|
export_csv
|
||||||
|
else
|
||||||
|
pexs = LifePex::Pex.where(user_id: current_user_id).map{ |pex| [pex, pex.user_pexs] }.to_h
|
||||||
|
api_response({ pexs: pexs, entity_type: "pexs" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
summary "Export every single user pex and pex as CSV"
|
||||||
|
produces "application/csv"
|
||||||
|
status_codes [200]
|
||||||
|
# parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
# parameter :date, required: true, type: "string", in: "path"
|
||||||
|
get "/export.csv", auth: [], provides: %w(application/csv) do
|
||||||
|
export_csv
|
||||||
|
end
|
||||||
|
|
||||||
|
private def export_csv
|
||||||
|
pexs = LifePex::Pex.where(user_id: current_user_id).order(:id).all
|
||||||
|
pexs_names = pexs.map { |pex| "[#{pex.category}] #{pex.name}" }
|
||||||
|
pexs_ids = pexs.map(&:id)
|
||||||
|
user_pexs_by_date = LifePex::UserPex
|
||||||
|
.where(Sequel.qualify(:user_pexs, :user_id) => current_user_id)
|
||||||
|
.order_by(:created_at).all.group_by(&:created_at)
|
||||||
|
csv_output = CSV.generate do |csv|
|
||||||
|
csv << ["date", *pexs_names]
|
||||||
|
user_pexs_by_date.each do |date, user_pexs|
|
||||||
|
amount_by_column = user_pexs.group_by(&:pex_id).transform_values { |v| v.size }
|
||||||
|
user_pex_amount_by_date_all_column = pexs_ids.map { |id| amount_by_column[id] || 0 }
|
||||||
|
csv << [date, *user_pex_amount_by_date_all_column]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
content_type "application/csv"
|
||||||
|
csv_output
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace "/pexs" do
|
||||||
|
|
||||||
|
summary "Get the amount of user_pex for a given day"
|
||||||
|
produces "application/json"
|
||||||
|
status_codes [200]
|
||||||
|
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(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count
|
||||||
|
api_response({ count: count, entity_type: "user_pex" })
|
||||||
|
end
|
||||||
|
|
||||||
|
summary "Create a new user_pex for a given day"
|
||||||
|
produces "application/json"
|
||||||
|
status_codes [200]
|
||||||
|
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 = 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"
|
||||||
|
status_codes [200]
|
||||||
|
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 = 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_destroyed: 1,
|
||||||
|
count_total: count_total,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
api_error(404, "Nothing to destroy")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
include LifePex::Systems::ApiList
|
||||||
|
end
|
1
src/systems/views
Symbolic link
1
src/systems/views
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../views/
|
79
src/utils/boot_framework.rb
Normal file
79
src/utils/boot_framework.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
|
# @example
|
||||||
|
# LifePex::BootFrameWork::Application.new(ENV).boot do
|
||||||
|
# harshly_need_env "VARIABLE_NAME1"
|
||||||
|
# kindly_ask_env "VARIABLE_NAME2" { do_something_if_failure() }
|
||||||
|
# harshly_do true
|
||||||
|
# kindly_do env["SOME_OTHER_VARIABLE"] { error "this should not happen :(((" }
|
||||||
|
# end.finish!
|
||||||
|
module LifePex::BootFramework
|
||||||
|
class Application
|
||||||
|
attr_reader :env, :stop
|
||||||
|
|
||||||
|
def initialize(env)
|
||||||
|
@env = env
|
||||||
|
@stop = false
|
||||||
|
end
|
||||||
|
|
||||||
|
private def message(string)
|
||||||
|
STDERR.puts string
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(string)
|
||||||
|
message "Error: #{string}".red
|
||||||
|
end
|
||||||
|
|
||||||
|
def warning(string)
|
||||||
|
message "Warning: #{string}".yellow
|
||||||
|
end
|
||||||
|
|
||||||
|
def boot(&block)
|
||||||
|
instance_eval(&block)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish!
|
||||||
|
if @stop
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def will_stop!
|
||||||
|
@stop = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def harshly_need_env(env_variable, &block)
|
||||||
|
@env.fetch env_variable do
|
||||||
|
error "\"#{env_variable}\" is a required ENV variable. Provide it.".red
|
||||||
|
will_stop!
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def kindly_ask_env(env_variable, &block)
|
||||||
|
@env.fetch env_variable do
|
||||||
|
warning "\"#{env_variable}\" is a prefered ENV variable. Provide it if possible.".yellow
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def harshly_do(test, &block)
|
||||||
|
if test
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
will_stop!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def kindly_do(test, &block)
|
||||||
|
if test
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.boot_application(&block)
|
||||||
|
Application.new(ENV).boot(&block)
|
||||||
|
end
|
||||||
|
end
|
11
src/utils/env.rb
Normal file
11
src/utils/env.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
require "dotenv"
|
||||||
|
|
||||||
|
def get_env
|
||||||
|
ENV.fetch("LIFEPEX_ENV") { "development" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_dotenv(app_env = nil)
|
||||||
|
app_env ||= get_env
|
||||||
|
Dotenv.load(".env.#{app_env}.local", ".env.local", ".env")
|
||||||
|
app_env
|
||||||
|
end
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "active_support/all"
|
||||||
|
|
||||||
module JSON::API
|
module JSON::API
|
||||||
def json_params
|
def json_params
|
||||||
begin
|
begin
|
||||||
|
@ -10,7 +12,103 @@ module JSON::API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
DATE_GENERATOR = {
|
||||||
|
"yesterday" => ->() { DateTime.now - 24.hours },
|
||||||
|
"now" => ->() { DateTime.now },
|
||||||
|
"today" => ->() { DateTime.now },
|
||||||
|
nil => ->() { DateTime.now },
|
||||||
|
}
|
||||||
|
# @param [String] date: either a date iso formatted or a word to be sent to DateTime
|
||||||
|
# @param [Float] offset: an amount of hours to remove from the date, useful for setting the start of the day hours after midnight
|
||||||
|
def date_input_convertor(date = "now", offset = 0.0)
|
||||||
|
base_time =
|
||||||
|
if DATE_GENERATOR.key?(date)
|
||||||
|
DATE_GENERATOR[date].call()
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
DateTime.parse date
|
||||||
|
rescue => _
|
||||||
|
DateTime.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
(base_time - offset.hours).to_date
|
||||||
|
end
|
||||||
|
|
||||||
|
# params:
|
||||||
|
# - mimes: example: %w(application/json text/json)
|
||||||
|
def accept?(*mimes)
|
||||||
|
@request_accept_str ||= request.accept.map(&:to_str)
|
||||||
|
@request_accept_str.intersection(mimes).size > 0
|
||||||
|
end
|
||||||
|
|
||||||
def accept_json?
|
def accept_json?
|
||||||
request.accept.any? { |a| a.entry == 'application/json' }
|
accept? 'application/json'
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept_html?
|
||||||
|
accept? 'text/html'
|
||||||
|
end
|
||||||
|
|
||||||
|
def slim_partial(partial, locals: {})
|
||||||
|
Slim::Template.new(
|
||||||
|
"src/views/partials/#{partial}.slim",
|
||||||
|
# layout: false,
|
||||||
|
).render(self, locals)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# render do
|
||||||
|
# json "{ ... }"
|
||||||
|
# html "< ... >"
|
||||||
|
# end
|
||||||
|
def given_content_type(&block)
|
||||||
|
raise "Invalid DSL call to JSON::API.render without block" unless block_given?
|
||||||
|
Render.new(self, &block).complete_rendering
|
||||||
|
end
|
||||||
|
|
||||||
|
class Render
|
||||||
|
delegate :slim,
|
||||||
|
:slim_partial,
|
||||||
|
# :api_error,
|
||||||
|
# :api_response,
|
||||||
|
# :render,
|
||||||
|
to: :@context
|
||||||
|
attr_reader :context
|
||||||
|
|
||||||
|
def initialize(context, &block)
|
||||||
|
@render = nil
|
||||||
|
@context = context
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_something_if_accept(acceptance, output, &block)
|
||||||
|
if block_given?
|
||||||
|
context.instance_eval(&block)
|
||||||
|
else
|
||||||
|
@render = output
|
||||||
|
end if @context.send("accept_#{acceptance}?")
|
||||||
|
end
|
||||||
|
|
||||||
|
def json(output = nil, &block)
|
||||||
|
render_something_if_accept :json, output, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def html(output = nil, &block)
|
||||||
|
render_something_if_accept :html, output, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def default(output = nil, &block)
|
||||||
|
if block_given?
|
||||||
|
@render_default_block = block
|
||||||
|
else
|
||||||
|
@render_default = output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete_rendering
|
||||||
|
@render ||
|
||||||
|
(@render_default_block.is_a?(Proc) && @context.instance_eval(&@render_default_block)) ||
|
||||||
|
@render_default
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
5
src/utils/string.rb
Normal file
5
src/utils/string.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class String
|
||||||
|
def camel_to_spaced
|
||||||
|
self.gsub(/([a-z])([A-Z])/, '\1 \2').capitalize
|
||||||
|
end
|
||||||
|
end
|
15
src/utils/users.rb
Normal file
15
src/utils/users.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module LifePex::UsersHelper
|
||||||
|
def my_user_pexs(cookies, date = nil)
|
||||||
|
params = {
|
||||||
|
user_id: user_id_decoded(cookies),
|
||||||
|
}
|
||||||
|
params[:created_at] = date unless date.nil?
|
||||||
|
LifePex::UserPex.where(params).all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_user_date
|
||||||
|
date = cookies["date"]
|
||||||
|
offset = cookies["late_day_offset"].to_f
|
||||||
|
date_input_convertor(date, offset)
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,7 +17,10 @@
|
||||||
I engage my honor to do never read or modify personnal data you may have put on
|
I engage my honor to do never read or modify personnal data you may have put on
|
||||||
the server, and do my best to ensure its security.
|
the server, and do my best to ensure its security.
|
||||||
You should look at the code source if you want to audit it.
|
You should look at the code source if you want to audit it.
|
||||||
br
|
|
||||||
|
h2
|
||||||
|
| GDPR
|
||||||
|
p
|
||||||
| In case you want to take a look at your data or want to delete it, I may in the futur
|
| In case you want to take a look at your data or want to delete it, I may in the futur
|
||||||
provide a feature for do it yourself.
|
provide a feature for do it yourself.
|
||||||
Meanwhile you can still drop me an issue or a message on
|
Meanwhile you can still drop me an issue or a message on
|
||||||
|
@ -26,6 +29,8 @@
|
||||||
|
|
|
|
||||||
i
|
i
|
||||||
| #lifepex.
|
| #lifepex.
|
||||||
|
a.btn.btn-success(href="/api/user-pex/v1/export.csv")
|
||||||
|
| Export as CSV
|
||||||
|
|
||||||
h2
|
h2
|
||||||
| Service version
|
| Service version
|
||||||
|
|
25
src/views/achievement_form.slim
Normal file
25
src/views/achievement_form.slim
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
|
h1
|
||||||
|
| Add a new Achievement
|
||||||
|
form.col-md-6 method="POST" action="/achievements"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="successName" value=success.name
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="inputName"
|
||||||
|
strong
|
||||||
|
| Name
|
||||||
|
input#inputUsername.form-control.form-control-lg name="inputName" type="text" /
|
||||||
|
- success.parameters.each do |param|
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="inputParams[#{param.name}]"
|
||||||
|
strong
|
||||||
|
= param.name.camel_to_spaced
|
||||||
|
- if param.required
|
||||||
|
| *
|
||||||
|
p=param.description
|
||||||
|
input.form-control.form-control-lg name="inputParams[#{param.name}]" type="text" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="inputMedal" Medal
|
||||||
|
input#inputMedal.form-control.form-control-lg name="inputMedal" type="text" placeholder="gold" /
|
||||||
|
.form-group.row
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="create"
|
35
src/views/achievements.slim
Normal file
35
src/views/achievements.slim
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
h1
|
||||||
|
| Current achievements
|
||||||
|
|
||||||
|
- LifePex::Achievement::AVAILABLE_SUCCESS.each do |available_success|
|
||||||
|
a.btn.btn-dark href="/achievements/new?successName=#{available_success}"
|
||||||
|
| Add a new "#{available_success}"
|
||||||
|
|
||||||
|
table.table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th Name
|
||||||
|
th Success Name
|
||||||
|
th Icon
|
||||||
|
th Parameters
|
||||||
|
th
|
||||||
|
tbody
|
||||||
|
- achievements.each do |achievement|
|
||||||
|
tr
|
||||||
|
td=achievement.name
|
||||||
|
td=achievement.success_name
|
||||||
|
td==achievement.medal
|
||||||
|
td
|
||||||
|
- achievement.parameters.each.with_index do |param_tuple, index|
|
||||||
|
strong
|
||||||
|
| #{param_tuple[0]}
|
||||||
|
| :
|
||||||
|
| #{param_tuple[1]}
|
||||||
|
- if index != achievement.parameters.size
|
||||||
|
br
|
||||||
|
td
|
||||||
|
form method="POST" action="/achievements/delete"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=achievement[:id]
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{achievement[:name]}\' ?')"
|
||||||
|
| x
|
|
@ -3,42 +3,22 @@
|
||||||
table.table
|
table.table
|
||||||
tbody
|
tbody
|
||||||
- pex_by_models.each.with_index do |pex_by_model, index|
|
- pex_by_models.each.with_index do |pex_by_model, index|
|
||||||
- if pex_by_model[:category] && pex_by_model[:category] != pex_by_models[index - 1][:category]
|
- if pex_by_model[:pex][:category] != pex_by_models[index - 1][:pex][:category]
|
||||||
tr.full-row-sep
|
tr.full-row-sep
|
||||||
td.col-12(colspan=6)
|
td.col-12(colspan=6)
|
||||||
- if pex_by_model[:category].empty?
|
- if pex_by_model[:pex][:category].empty?
|
||||||
| Base
|
| Base
|
||||||
- else
|
- else
|
||||||
= pex_by_model[:category].capitalize
|
= pex_by_model[:pex][:category].capitalize
|
||||||
tr.full-row
|
- @row_class = ""
|
||||||
td.col-1
|
- if pex_by_model[:pex][:hidden]
|
||||||
.btn.pex-editor-toggler(name=pex_by_model[:id] style="display: none;")
|
- @row_class = "bg-warning"
|
||||||
| ¤
|
- if pex_by_model[:pex][:flag] == "bookmarked"
|
||||||
.pex-editor(name=name=pex_by_model[:id])
|
- @row_class = "bg-bookmarked"
|
||||||
form method="POST" action="/pexs/delete"
|
== slim :"partials/pex_row", locals: { pex_with_amount: pex_by_model }
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
input type="hidden" name="type" value="-"
|
|
||||||
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permantly REMOVE \'#{pex_by_model[:name]}\' and points ?')"
|
|
||||||
| x
|
|
||||||
a.btn.btn-warning href="/pexs/update?id=#{pex_by_model[:id]}"
|
|
||||||
| u
|
|
||||||
td.col-8=pex_by_model[:name]
|
|
||||||
td.col-1.center
|
|
||||||
form method="POST" action="/"
|
|
||||||
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]
|
|
||||||
td.col-1.center
|
|
||||||
form method="POST" action="/"
|
|
||||||
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 method="GET" action="/pexs"
|
form method="GET" action="/pexs"
|
||||||
|
== csrf_tag
|
||||||
button.float-end.round.custom-valid
|
button.float-end.round.custom-valid
|
||||||
| +
|
| +
|
||||||
|
|
||||||
|
|
|
@ -2,49 +2,74 @@ 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
|
||||||
link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" rel="stylesheet" /
|
link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" rel="stylesheet" /
|
||||||
link href="/css/bootstrap-override.css" rel="stylesheet"
|
link href="/css/bootstrap-override.css" rel="stylesheet"
|
||||||
link href="/css/main.css" rel="stylesheet"
|
link href="/css/main.css" rel="stylesheet"
|
||||||
|
link rel="shortcut icon" type="image/png" href="/img/favicon.png" /
|
||||||
|
|
||||||
body
|
body
|
||||||
header.bg-dark
|
header.bg-dark
|
||||||
nav.navbar.navbar-expand
|
nav.navbar.navbar-expand-sm.navbar-dark.bg-dark
|
||||||
.container-fluid
|
.container-fluid
|
||||||
ul.navbar-nav
|
- if logged_in?
|
||||||
|
- if cookies["date"] == "yesterday"
|
||||||
|
a.navbar-brand href="/"
|
||||||
|
| Yesterday
|
||||||
|
- if cookies["show_full_date"] == "on"
|
||||||
|
.smaller= get_user_date
|
||||||
|
- else
|
||||||
|
a.navbar-brand href="/"
|
||||||
|
| Today
|
||||||
|
- if cookies["show_full_date"] == "on"
|
||||||
|
.smaller= get_user_date
|
||||||
|
button.navbar-toggler type="button" data-bs-toggle="collapse" data-bs-target="#navbar-collapser" aria-controls="navbar-collapser" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
|
span.navbar-toggler-icon/
|
||||||
|
.collapse.navbar-collapse#navbar-collapser
|
||||||
|
ul.navbar-nav.me-auto.mb-2.mb-lg-0
|
||||||
- if logged_in?
|
- if logged_in?
|
||||||
- if cookies["date"] == "yesterday"
|
- if cookies["date"] == "yesterday"
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/today" Today
|
a.btn.btn-lg.btn-dark href="/today" Today
|
||||||
li.nav-item
|
|
||||||
a.btn.btn-lg.btn-dark.active href="/" Yesterday
|
|
||||||
- else
|
- else
|
||||||
li.nav-item
|
|
||||||
a.btn.btn-lg.btn-dark.active href="/" Today
|
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/yesterday" Yesterday
|
a.btn.btn-lg.btn-dark href="/yesterday" Yesterday
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/recap" Recap
|
a.btn.btn-lg.btn-dark.position-relative href="/recap"
|
||||||
li.nav-item.dropdown
|
| Recap
|
||||||
a.btn.btn-lg.btn-dark.nav-link.dropdown-toggle#nav-drop-more href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"
|
- if (recalls_count = current_user.recalls_not_validated.count) > 0
|
||||||
| More
|
span.position-absolute.top-0.start-100.translate-middle.badge.rounded-pill.bg-danger
|
||||||
ul.dropdown-menu aria-labelledby="nav-drop-more"
|
= recalls_count
|
||||||
li
|
li.nav-item
|
||||||
|
a.btn.btn-lg.btn-dark href="/achievements" Achievements
|
||||||
|
li.nav-item
|
||||||
|
a.btn.btn-lg.btn-dark href="/recalls" Recalls
|
||||||
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/password" Change password
|
a.btn.btn-lg.btn-dark href="/password" Change password
|
||||||
li
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
||||||
li
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/logout" Logout
|
a.btn.btn-lg.btn-dark href="/preferences" Profil preference
|
||||||
|
li.nav-item
|
||||||
|
a.btn.btn-lg.btn-dark href="/?filter_hidden=false" Show hidden
|
||||||
|
li.nav-item
|
||||||
|
form method="POST" action="/logout"
|
||||||
|
input.btn.btn-lg.btn-dark type="submit" value="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
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/register" Register
|
a.btn.btn-lg.btn-dark href="/register" Register
|
||||||
|
li.nav-item
|
||||||
|
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}"
|
||||||
|
@ -57,6 +82,8 @@ html lang="en"
|
||||||
.footer
|
.footer
|
||||||
.container-sm
|
.container-sm
|
||||||
|
|
||||||
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"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
h1 Sign in with your account
|
h1 Sign in with your account
|
||||||
form.col-md-6 method="POST" action="/login"
|
form#login.col-md-6 method="POST" action="/login"
|
||||||
|
== csrf_tag
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-2.col-form-label for="inputUsername" Username
|
label.col-sm-2.col-form-label for="inputUsername" Username
|
||||||
input#inputUsername.form-control.form-control-lg name="username" type="text" /
|
input#inputUsername.form-control.form-control-lg name="username" type="text" /
|
||||||
|
|
53
src/views/partials/pex_row.slim
Normal file
53
src/views/partials/pex_row.slim
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
tr.full-row(class=@row_class)
|
||||||
|
td.col-1
|
||||||
|
.btn.pex-editor-toggler(name=pex_with_amount[:pex][:id] style="display: none;")
|
||||||
|
| ¤
|
||||||
|
.pex-editor(name=name=pex_with_amount[:pex][:id])
|
||||||
|
form method="POST" action="/pexs/bookmark"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
button.btn.btn-success type="submit"
|
||||||
|
- if pex_with_amount[:pex][:flag] == "bookmarked"
|
||||||
|
| unfav
|
||||||
|
- else
|
||||||
|
| fav
|
||||||
|
form method="POST" action="/pexs/delete"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="-"
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{pex_with_amount[:pex][:name]}\' and points ?')"
|
||||||
|
| x
|
||||||
|
form method="POST" action="/pexs/hide"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
- if pex_with_amount[:pex][:hidden]
|
||||||
|
button.btn.btn-success type="submit"
|
||||||
|
| >>
|
||||||
|
- else
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to disable \'#{pex_with_amount[:pex][:name]}\' (do not affect points) ?')"
|
||||||
|
| <<
|
||||||
|
a.btn.btn-warning href="/pexs/update?id=#{pex_with_amount[:pex][:id]}"
|
||||||
|
| u
|
||||||
|
td.col-8
|
||||||
|
.row
|
||||||
|
.col-8
|
||||||
|
| #{pex_with_amount[:pex][:name]}
|
||||||
|
.col-4
|
||||||
|
.small=pex_with_amount[:user_pexs][:last_inserted_at]
|
||||||
|
td.col-1.center
|
||||||
|
form.userpexvalidation.userpexvalidationdecrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="-"
|
||||||
|
button.btn.force-1-col type="submit" style="display: block;"
|
||||||
|
| -
|
||||||
|
td.col-1.center
|
||||||
|
.userpexvalidation.userpexvalidationvalue
|
||||||
|
=pex_with_amount[:user_pexs][:at_date]
|
||||||
|
td.col-1.center
|
||||||
|
form.userpexvalidation.userpexvalidationincrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="+"
|
||||||
|
button.btn.force-1-col type="submit" style="display: block;"
|
||||||
|
| +
|
|
@ -1,6 +1,7 @@
|
||||||
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
h1 Change your password
|
h1 Change your password
|
||||||
form.col-md-6 method="POST" action="/password"
|
form.col-md-6 method="POST" action="/password"
|
||||||
|
== csrf_tag
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-2.col-form-label for="inputPassword" Password
|
label.col-sm-2.col-form-label for="inputPassword" Password
|
||||||
input#inputPassword.form-control.form-control-lg name="password" type="password" minlength="5" /
|
input#inputPassword.form-control.form-control-lg name="password" type="password" minlength="5" /
|
||||||
|
|
|
@ -7,6 +7,7 @@ main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
- else
|
- else
|
||||||
| Add a new pex
|
| Add a new pex
|
||||||
form.col-md-6 method="POST" action="/pexs"
|
form.col-md-6 method="POST" action="/pexs"
|
||||||
|
== csrf_tag
|
||||||
- if pex
|
- if pex
|
||||||
input type="hidden" name="id" value=pex.id
|
input type="hidden" name="id" value=pex.id
|
||||||
.form-group.row
|
.form-group.row
|
||||||
|
@ -14,7 +15,10 @@ main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
input#inputUsername.form-control.form-control-lg name="name" type="text" value=pex&.name /
|
input#inputUsername.form-control.form-control-lg name="name" type="text" value=pex&.name /
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-12.col-form-label for="inputCategory" Category
|
label.col-sm-12.col-form-label for="inputCategory" Category
|
||||||
input#inputCategory.form-control.form-control-lg name="category" type="text" placeholder="sport" value=pex&.category /
|
input#inputCategory.form-control.form-control-lg name="category" type="text" placeholder="sport" list="categoryList" value=pex&.category /
|
||||||
|
datalist#categoryList
|
||||||
|
- categories.each do |category|
|
||||||
|
option value=category
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-12.col-form-label for="inputAmount" Xp Amount by check
|
label.col-sm-12.col-form-label for="inputAmount" Xp Amount by check
|
||||||
input#inputAmount.form-control.form-control-lg name="amount" type="integer" min="-50" max="50" value=(pex&.amount||"1") /
|
input#inputAmount.form-control.form-control-lg name="amount" type="integer" min="-50" max="50" value=(pex&.amount||"1") /
|
||||||
|
|
45
src/views/preferences.slim
Normal file
45
src/views/preferences.slim
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
.container
|
||||||
|
h1
|
||||||
|
| Profil preferences
|
||||||
|
|
||||||
|
h2
|
||||||
|
| Recap
|
||||||
|
form(method="POST")
|
||||||
|
== csrf_tag
|
||||||
|
/-
|
||||||
|
- USER_PREFERENCES.each do |param_name, upc|
|
||||||
|
- if upc.html[:type] == "checkbox"
|
||||||
|
.form-group.form-check.form-switch
|
||||||
|
label.col-sm-12.form-check-label for=param_name
|
||||||
|
strong=upc.description
|
||||||
|
- if cookies[upc.cookie] == "on"
|
||||||
|
input.form-check-input *upc.html name=param_name checked="on" /
|
||||||
|
- else
|
||||||
|
input.form-check-input *upc.html name=param_name /
|
||||||
|
- else
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for=param_name
|
||||||
|
strong=upc.description
|
||||||
|
input.form-control.form-control-lg *upc.html name=param_name value=cookies[upc.cookie] /
|
||||||
|
.form-group.row
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="Update"
|
||||||
|
|
||||||
|
- if LifePex::APP_ENV == "debug"
|
||||||
|
h2
|
||||||
|
| Debug
|
||||||
|
p
|
||||||
|
table
|
||||||
|
- cookies.each do |k, v|
|
||||||
|
tr
|
||||||
|
td= "cookies.#{k}"
|
||||||
|
td= v
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
| get_user_date
|
||||||
|
td= get_user_date
|
||||||
|
|
||||||
|
h2
|
||||||
|
| Private data export
|
||||||
|
a.btn.btn-success(href="/api/user-pex/v1/export.csv")
|
||||||
|
| Export as CSV
|
||||||
|
|
33
src/views/recall_form.slim
Normal file
33
src/views/recall_form.slim
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
|
h1
|
||||||
|
| Add a new Recall
|
||||||
|
form.col-md-6 method="POST" action="/recalls"
|
||||||
|
== csrf_tag
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="name"
|
||||||
|
strong
|
||||||
|
| Name
|
||||||
|
input#name.form-control.form-control-lg name="name" type="text" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="span_duration"
|
||||||
|
strong
|
||||||
|
| Span duration
|
||||||
|
input#span_duration.form-control.form-control-lg name="span_duration" type="number" value="7" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="repeated"
|
||||||
|
strong
|
||||||
|
| Repeated
|
||||||
|
input#inputRepeated.form-control.form-control-lg name="repeated" type="number" value="1" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="pex_id"
|
||||||
|
strong
|
||||||
|
| PexId
|
||||||
|
select.form-select.form-control-lg arial-label="Pex Id" name="pex_id"
|
||||||
|
- pexs.each.with_index do |pex, pex_idx|
|
||||||
|
- if pex_idx == 0
|
||||||
|
option value=pex[:id] selected="true" =pex[:name]
|
||||||
|
- else
|
||||||
|
option value=pex[:id] =pex[:name]
|
||||||
|
|
||||||
|
.form-group.row
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="create"
|
25
src/views/recalls.slim
Normal file
25
src/views/recalls.slim
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
h1
|
||||||
|
| Current recalls
|
||||||
|
|
||||||
|
a.btn.btn-dark href="/recalls/new"
|
||||||
|
| Add a new recall
|
||||||
|
|
||||||
|
table.table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th Name
|
||||||
|
th Span duration
|
||||||
|
th Repeated
|
||||||
|
th
|
||||||
|
tbody
|
||||||
|
- recalls.each do |recall|
|
||||||
|
tr
|
||||||
|
td=recall.name
|
||||||
|
td=recall.span_duration
|
||||||
|
td=recall.repeated
|
||||||
|
td
|
||||||
|
form method="POST" action="/recalls/delete"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=recall[:id]
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{recall[:name]}\' ?')"
|
||||||
|
| x
|
|
@ -2,6 +2,17 @@ script src="https://code.highcharts.com/highcharts.js"
|
||||||
|
|
||||||
h1 Recap
|
h1 Recap
|
||||||
|
|
||||||
|
.recap-recalls
|
||||||
|
- if !recalls_not_validated.empty?
|
||||||
|
p
|
||||||
|
| You have a warning to consider!
|
||||||
|
|
||||||
|
h2
|
||||||
|
- recalls_not_validated.each do |recall|
|
||||||
|
span.badge.bg-danger
|
||||||
|
| #{recall[:name]}
|
||||||
|
br/
|
||||||
|
|
||||||
.recap-level
|
.recap-level
|
||||||
span.badge.bg-primary
|
span.badge.bg-primary
|
||||||
| Level #{level.current_level.round}
|
| Level #{level.current_level.round}
|
||||||
|
@ -10,6 +21,23 @@ h1 Recap
|
||||||
| #{level.xp_from_current_level.round} / #{level.xp_for_complete_level.round}
|
| #{level.xp_from_current_level.round} / #{level.xp_for_complete_level.round}
|
||||||
|
|
||||||
.recap-xp
|
.recap-xp
|
||||||
|
.float-end
|
||||||
|
form#reloader.col-md-6 method="GET"
|
||||||
|
== csrf_tag
|
||||||
|
.form-group.row
|
||||||
|
label.col-form-label for="inputDaysAgo" Load how many days since today ?
|
||||||
|
input#inputDaysAgo.form-control.form-control-lg name="days_ago" type="number" min="3" max="365" step="1" value=(params["days_ago"] || cookies["recap_days"] || "60") /
|
||||||
|
.form-group.row
|
||||||
|
p
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="Reload"
|
||||||
#recap-xp-container style="width:100%; height:400px;"
|
#recap-xp-container style="width:100%; height:400px;"
|
||||||
|
|
||||||
|
.recap-success
|
||||||
|
- if defined?(medals)
|
||||||
|
- medals.each do |medal|
|
||||||
|
p
|
||||||
|
| You have the achievement "#{medal.name}"
|
||||||
|
== medal.medal
|
||||||
|
| !
|
||||||
|
|
||||||
script src="/js/recap.js"
|
script src="/js/recap.js"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
h1 Register a new account
|
h1 Register a new account
|
||||||
p.bg-warning
|
p.bg-warning
|
||||||
| Warning: There is no password recovery or change possible yet
|
| Warning: There is no password recovery. Don't loose it.
|
||||||
form.col-md-6 method="POST" action="/register"
|
form.col-md-6 method="POST" action="/register"
|
||||||
|
== csrf_tag
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-2.col-form-label for="inputUsername" Username
|
label.col-sm-2.col-form-label for="inputUsername" Username
|
||||||
input#inputUsername.form-control.form-control-lg name="username" type="text" /
|
input#inputUsername.form-control.form-control-lg name="username" type="text" /
|
||||||
|
|
82
test/base.rb
Normal file
82
test/base.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
ENV["LIFEPEX_ENV"] = "test"
|
||||||
|
require "yaml"
|
||||||
|
require_relative "./fixtures_reader"
|
||||||
|
require_relative "../src/app"
|
||||||
|
|
||||||
|
require "rack/test"
|
||||||
|
require "test/unit"
|
||||||
|
|
||||||
|
class LifePexTest < Test::Unit::TestCase
|
||||||
|
include Rack::Test::Methods
|
||||||
|
|
||||||
|
def app
|
||||||
|
LifePex::App
|
||||||
|
end
|
||||||
|
|
||||||
|
%i(get head).each do |verb|
|
||||||
|
define_method "api_#{verb}" do |uri, **headers|
|
||||||
|
self.send(verb, uri, headers.merge({ "CONTENT_TYPE" => "application/json" }))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i(post put patch delete).each do |verb|
|
||||||
|
define_method "api_#{verb}" do |uri, body = {}, **headers|
|
||||||
|
header "Accept", "application/json"
|
||||||
|
header "Content-Type", "application/json"
|
||||||
|
self.send(verb, uri, body.to_json, headers.merge({
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_as(username = "test", password = "test")
|
||||||
|
api_post '/api/user/v1/login', { username: username, password: password }
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if every key of `minimal_data` is contained in `full_data`
|
||||||
|
def assert_hash_include(minimal_data, full_data)
|
||||||
|
full_data = full_data.stringify_keys
|
||||||
|
minimal_data.stringify_keys.each do |key, value|
|
||||||
|
assert_equal value, full_data[key], "#{key} should contains <#{value}>, got <#{full_data[key]}>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# parse the body as a json
|
||||||
|
def api_reponse_body
|
||||||
|
JSON.parse last_response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if the body contains a message
|
||||||
|
def assert_message_exist
|
||||||
|
assert_not_empty api_reponse_body.dig "message"
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if the entity.user_id belongs to the user who sent the last req
|
||||||
|
def assert_entity_owned(entity)
|
||||||
|
user_id = JWT.decode(last_request.cookies["auth"], nil, false).first["user_id"]
|
||||||
|
assert_equal user_id.to_s, entity["user_id"].to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# status may be a Regexp: the assertion with test if they match
|
||||||
|
# status may be a String: the assertion with test if the param includes the response status
|
||||||
|
# status may be another thing: the assertion with test equality
|
||||||
|
def assert_status(status)
|
||||||
|
if status.is_a?(Regexp)
|
||||||
|
assert_match status, last_response.status.to_s
|
||||||
|
elsif status.is_a?(String)
|
||||||
|
assert_equal status, last_response.status.to_s[0..(status.size)]
|
||||||
|
else
|
||||||
|
assert_equal status, last_response.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_client_error(status = /^4\d\d/)
|
||||||
|
refute last_response.ok?
|
||||||
|
assert_status status
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_server_error(status = /^5\d\d/)
|
||||||
|
refute last_response.ok?
|
||||||
|
assert_status status
|
||||||
|
end
|
||||||
|
end
|
6
test/fixtures/pex_base.yaml
vendored
Normal file
6
test/fixtures/pex_base.yaml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pexs:
|
||||||
|
- id: 1
|
||||||
|
user_id: 1
|
||||||
|
name: 'pex_test_1'
|
||||||
|
category: 'category'
|
||||||
|
amount: 1.234
|
7
test/fixtures/user_base.yaml
vendored
Normal file
7
test/fixtures/user_base.yaml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
users:
|
||||||
|
- id: 1
|
||||||
|
username: "test"
|
||||||
|
hashed_password: "$2a$12$yWy1fyQBTGYfwRY7H8QGAubHS3nJiMWAaHl8HhXZcTZnuQxRlqIhu" # test
|
||||||
|
- id: 2
|
||||||
|
username: "testbis"
|
||||||
|
hashed_password: "$2a$12$yWy1fyQBTGYfwRY7H8QGAubHS3nJiMWAaHl8HhXZcTZnuQxRlqIhu" # test
|
23
test/fixtures_reader.rb
Normal file
23
test/fixtures_reader.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
class FixtureReader
|
||||||
|
def initialize
|
||||||
|
cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
KEEP_TABLES = %i(meta)
|
||||||
|
|
||||||
|
def apply!(id)
|
||||||
|
path = File.join("test", "fixtures", "#{id}.yaml")
|
||||||
|
data = YAML.load_file path
|
||||||
|
data.each do |table, rows|
|
||||||
|
rows.each { |row| LifePex::DB[table.to_sym].insert row }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup!
|
||||||
|
LifePex::DB.tables.reject { |table, _| KEEP_TABLES.include?(table) }.each do |table|
|
||||||
|
LifePex::DB[table].truncate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
149
test/pex2_system_test.rb
Normal file
149
test/pex2_system_test.rb
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class Pex2SystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
private def login_as(username = "test", password = "test")
|
||||||
|
api_post '/api/user/v1/login', { username: username, password: password }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_life_cycle
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body
|
||||||
|
|
||||||
|
pex_input = { name: "pex1", category: "cat1", amount: 1.23 }
|
||||||
|
api_post "/api/pex/v2/pexs", pex_input
|
||||||
|
assert last_response.ok?, "A new pex should have been added"
|
||||||
|
assert_message_exist
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body.size, "A new pex should exist"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}/more"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_errors
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
assert_client_error 401 # not connect
|
||||||
|
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { }
|
||||||
|
assert_client_error 400 # missing name
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { amount: 1.23 }
|
||||||
|
assert_client_error 400 # missing name
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1' }
|
||||||
|
assert_client_error 400 # missing amount
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
assert_client_error # duplicate
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_security
|
||||||
|
login_as "test"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
login_as "testbis"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert_client_error
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}/more"
|
||||||
|
assert_client_error
|
||||||
|
api_put "/api/pex/v2/pexs/#{pex_id}", {}
|
||||||
|
assert_client_error
|
||||||
|
api_delete "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert_client_error
|
||||||
|
api_post "/api/pex/v2/pexs/#{pex_id}/recap", {}
|
||||||
|
assert_client_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_validation_and_counts
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs/#{pex_id}/recap", {}
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body["user_pexs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_update
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_put "/api/pex/v2/pexs/#{pex_id}", { name: 'pex2', amount: 1.25 }
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal pex_id, api_reponse_body.dig("pex", "id")
|
||||||
|
assert_equal "pex2", api_reponse_body.dig("pex", "name")
|
||||||
|
assert_equal 1.25, api_reponse_body.dig("pex", "amount")
|
||||||
|
assert_equal "cat1", api_reponse_body.dig("pex", "category")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_delete
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body.size, "No pex exist yet"
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body.size, "A pex should exists now"
|
||||||
|
|
||||||
|
api_delete "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body.size, "Pex should be removed"
|
||||||
|
end
|
||||||
|
end
|
42
test/user_pex_test.rb
Normal file
42
test/user_pex_test.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class UserPexSystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
@fixture.apply! "pex_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_user_pex_counter
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body["count"], "No user pex counted yet"
|
||||||
|
|
||||||
|
api_post "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal "2021-01-01", api_reponse_body.dig("user_pex", "created_at")
|
||||||
|
assert_equal 1, api_reponse_body.dig("user_pex", "pex_id")
|
||||||
|
assert_equal 1, api_reponse_body.dig("user_pex", "user_id")
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body["count"], "One use pex is added"
|
||||||
|
|
||||||
|
api_delete "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body["count"], "No user pex remaining"
|
||||||
|
|
||||||
|
api_delete "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert_client_error
|
||||||
|
end
|
||||||
|
end
|
37
test/user_system_test.rb
Normal file
37
test/user_system_test.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class UserSystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_login_generates_cookie
|
||||||
|
data = { username: 'test', password: 'test' }
|
||||||
|
api_post '/api/user/v1/login', data
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_not_empty last_response.headers["Set-Cookie"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_register_and_login
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_not_empty last_response.headers["Set-Cookie"]
|
||||||
|
assert_match (/auth=[\-\.\w]+; domain=example.org; path=\//), last_response.headers["Set-Cookie"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_register_refuse_duplicate
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
refute last_response.ok?
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user