Compare commits

...

13 Commits

Author SHA1 Message Date
Arthur POULET c2b5dd96b2
pex: add date of last insert
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-11-13 12:08:33 +01:00
Arthur POULET 5a24cdb6b7
deps: comment pg because sqlite is default
continuous-integration/drone/push Build is passing Details
2021-08-17 13:16:32 +02:00
Arthur POULET ab708456ed
drone: fix automated test
continuous-integration/drone/push Build is passing Details
2021-08-17 13:14:02 +02:00
Arthur POULET f2ad52586f
env: fix scripts env loading 2021-08-17 13:10:53 +02:00
Arthur POULET d1fae9bf63
script: improve db migration 2021-08-17 12:52:09 +02:00
Arthur POULET fb02993fa3
recap: add xAxis infos for charts dates 2021-08-14 21:57:37 +02:00
Arthur POULET b36249ae76
recap: add input to control how much data to fetch 2021-08-11 09:13:53 +02:00
Arthur POULET 2559c8c7a7
recap: improve recap by limiting amount of data output 2021-08-11 01:26:23 +02:00
Arthur POULET fd23535924
pex: fix update form
continuous-integration/drone/push Build was killed Details
2021-07-03 13:08:27 +02:00
Arthur POULET 0e26243315
pex: include an autocomplete in pex form
continuous-integration/drone/push Build is passing Details
2021-07-03 13:00:35 +02:00
Arthur POULET 9315c422a0
recalls: add recall warning on recap
continuous-integration/drone/push Build is passing Details
2021-07-03 12:43:34 +02:00
Arthur POULET 08c895f3a5
recalls: add recall link in nav bar 2021-07-03 12:23:44 +02:00
Arthur POULET 52a8d4b5fe
recalls: add recalls crd 2021-07-03 12:23:00 +02:00
24 changed files with 356 additions and 74 deletions

View File

@ -9,5 +9,5 @@ steps:
LIFEPEX_ENV: "test"
commands:
- bundle install --jobs=1 --retry=1
- ./init/database.rb
- rake db:migrate
- rake test

View File

@ -9,10 +9,10 @@ gem "sinatra-contrib", "~> 2.1"
gem "slim", "~> 4.1"
# database
# you # comment what you don't want
gem "sequel", "~> 5.43"
# you # comment the drivers you don't want
gem "sqlite3", "~> 1.4"
gem "pg", "~> 1.2"
# gem "pg", "~> 1.2"
# security
gem "jwt", "~> 2.2"

View File

@ -46,7 +46,7 @@ Since I suck with SGDB administration, this is a sample of what you may do.
psql -U postgres postgres -c "CREATE USER root WITH PASSWORD 'toor' SUPERUSER;"
psql -U postgres postgres -c "CREATE DATABASE life_pex"
echo LIFEPEX_DB=postgres://root:toor@localhost/life_pex >> .env.local
./init/database.rb
rake db:migrate
```
### Start
@ -91,7 +91,7 @@ editor .env.test.local # don't forget to set a new database !!!
Then init the database
```
LIFEPEX_ENV=test ./init/database.rb
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).

View File

@ -5,4 +5,17 @@ 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

13
init/load_env.rb Normal file
View 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

View File

@ -1,41 +1,30 @@
#!/usr/bin/env ruby
require "sequel"
require "colorize"
require "dotenv"
Dotenv.load(".env.local", ".env")
require_relative "../src/utils/semver.rb"
DB = Sequel.connect ENV["LIFEPEX_DB"]
if ENV["environment"] == "debug"
require "pry"
binding.pry
end
require_relative "./load_env"
def current_version
DB[:meta].first[:version] rescue 0
end
def migrate(version, &block)
puts "Check migration #{version}"
def migrate(version, message = nil, &block)
puts "Check migration #{version}".on_blue
if current_version < version
puts "Migrate #{version}".blue
begin
yield
DB[:meta].update(version: version)
puts "Successfuly set version #{version}"
puts "Successfuly set version #{version}".green
puts message.green if message
rescue => err
puts err.message.red
puts err.message.on_red
exit 1
end
else
puts "Already migrated #{version}".blue
puts "Already migrated #{version}".yellow
end
end
migrate 1 do
migrate 1, "Initialized database" do
DB.create_table :meta do
primary_key :id
Int :version
@ -60,33 +49,29 @@ migrate 1 do
String :username
String :hashed_password
end rescue puts "users already exists".yellow
puts "Initialized database".green
end
migrate 2 do
migrate 2, "Add pex categories" do
DB.alter_table :pexs do
add_column :category, String
end rescue puts "pexs.category already exists".yellow
puts "Migrated categories".green
end
migrate 3 do
migrate 3, "Add pex user belonging" do
DB.alter_table :pexs do
add_column :user_id, :Int
end rescue puts "pexs.user_id already exists".yellow
DB[:pexs].update(user_id: 1)
puts "Migrated pex belonging to user".green
end
migrate 4 do
migrate 4, "Fix pex category default behavior" do
DB.alter_table :pexs do
set_column_default(:category, '')
end rescue puts "pexs.category default already exists".yellow
DB[:pexs].where(category: nil).update(category: '')
puts "Migrated pex default category".green
end
migrate 5 do
migrate 5, "Add meta schema" do
DB.alter_table :meta do
add_column :code_version, String
end rescue puts "meta.code_version already exists".yellow
@ -99,18 +84,20 @@ migrate 6 do
end rescue puts "meta.code_date already exists".yellow
end
migrate 7 do
DB[:pexs].each { |pex| DB[:pexs].where(id: pex[:id]).update(category: pex[:category].to_s.downcase) }
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 do
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 do
migrate 9, "Add achievements" do
DB.create_table :achievements do
primary_key :id
Int :user_id
@ -121,14 +108,25 @@ migrate 9 do
end
end
migrate 10 do
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
puts "Migrated pex flag for bookmarks".green
end
puts "End migration".green
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_DATE=`git show #{CODE_VERSION} --pretty="format:%as"`.split("\n").first
DB[:meta].update(code_version: CODE_VERSION)

View File

@ -1,4 +1,4 @@
function setupChart(...cumuls) {
function setupChart({ all_dates }, ...cumuls) {
const chart = Highcharts.chart('recap-xp-container', {
chart: {
backgroundColor: '#000',
@ -7,6 +7,9 @@ function setupChart(...cumuls) {
text: '',
color: '#55f5f5',
},
xAxis: {
categories: all_dates,
},
series: cumuls.map((cumul, idx) => ({
// color: '#55f5f5',
visible: idx != 0,
@ -16,15 +19,41 @@ function setupChart(...cumuls) {
});
}
document.addEventListener('DOMContentLoaded', async function () {
function objectToQueryParam(obj) {
return Object.entries(obj).map(tuple => tuple.join("=")).join("&");
}
function urlWithQueryParams(base, object_with_params) {
if (Object.keys(object_with_params).length > 0) {
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: "/api/pex/v1/recap",
url: url,
body: null,
headers: { Accept: "application/json" },
on_success: (body) => {
const json_output = JSON.parse(body);
setupChart(...json_output.pex_tables);
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();
});
});

View File

@ -18,16 +18,15 @@ require "active_support"
require "active_support/core_ext"
require "pry" # debug
# Load environment variables
require_relative "./utils/env"
module LifePex
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
APP_ENV = load_dotenv
end
require "dotenv"
Dotenv.load(".env.#{LifePex::APP_ENV}.local", ".env.local", ".env")
# 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"
@ -40,6 +39,7 @@ LifePex::BootFramework.boot_application do
end
end.finish!
# Setup module variables
module LifePex
DB = Sequel.connect ENV["LIFEPEX_DB"]
BASE_URL = ENV["LIFEPEX_BASE_URL"] || "http://localhost:4567"
@ -53,6 +53,8 @@ module LifePex
end
end
# Then we load all the systems
Dir[File.join(__dir__, 'utils', '*.rb')].each { |file| require file }
require_relative "./achievements/dsl.rb"
@ -61,10 +63,12 @@ require_relative "./achievements/achievement.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
set :public_folder, "public"
end
# Main app
class LifePex::App < Sinatra::Base
DocMyRoutes.configure do |config|
config.title = "LifePex"

View File

@ -1,6 +1,7 @@
class LifePex::Pex < Sequel::Model(LifePex::DB[:pexs].order(:category, :name))
one_to_many :user_pexs
many_to_one :user
one_to_many :recalls
# note: wont work on #update
def before_validation

4
src/models/recall.rb Normal file
View File

@ -0,0 +1,4 @@
class LifePex::Recall < Sequel::Model(LifePex::DB[:recalls].order(:name))
many_to_one :user
many_to_one :pex
end

View File

@ -1,6 +1,7 @@
class LifePex::User < Sequel::Model(:users)
one_to_many :user_pexs
one_to_many :pexs
one_to_many :recalls
def password=(clear_password)
self.hashed_password = BCrypt::Password.create(clear_password)

View File

@ -1,4 +1,12 @@
class LifePex::UserPex < Sequel::Model(:user_pexs)
many_to_one :user
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

View File

@ -37,7 +37,7 @@ class LifePex::Systems::AuthSystem < Sinatra::Base
condition do
unless logged_in?
if accept_json?
halt 401, { 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
redirect "/login", 303
end

View File

@ -39,13 +39,18 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
user_pexs = my_user_pexs(cookies, get_user_date)
user_pexs_amounts = user_pexs.group_by { |user_pex| user_pex[:pex_id] }
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: pex,
user_pexs: {
at_date: (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
@ -71,12 +76,22 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
end
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
get "/pexs/update", auth: [] do
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
post "/pexs", auth: [] do
@ -118,19 +133,28 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
end
end
def get_recap_infos(cookies)
def get_recap_infos(params, cookies)
user_pexs = my_user_pexs(cookies)
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 }
level = LifePex::Level.new total_xp
all_dates = user_pexs.map(&:created_at).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
pexs.values.each do |pex|
pex[:total_by_date].each do |date, total|
# 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
@ -146,7 +170,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
category = pex[: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] += total
pex_table_by_category_and_date[category][date] += total if pex_table_by_category_and_date[category][date]
end
end
@ -161,6 +185,15 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
}
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)
@ -168,13 +201,16 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
{
pex_tables: highchart_tables,
start_date: start_date,
all_dates: all_dates,
level: level,
medals: medals,
recalls_not_validated: recalls_not_validated,
}
end
get "/recap", auth: [], provides: 'html' do
recap_infos = get_recap_infos(cookies)
recap_infos = get_recap_infos(params, cookies)
slim :recap, locals: recap_infos
end
@ -185,9 +221,10 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
produces 'application/json'
status_codes [200]
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],
all_dates: recap_infos[:all_dates],
level: recap_infos[:level].to_h,
}.to_json
end

View File

@ -25,7 +25,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
produces 'application/json'
status_codes [200]
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 = pexs.select(json_params["pluck"]) if json_params["pluck"]
pexs.to_json
@ -38,7 +38,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
parameter :category, required: true, type: 'string', in: 'body'
parameter :amount, required: true, type: 'number', in: 'body'
parameter :auth, required: true, type: 'string', in: 'cookies'
post "", auth: [], provides: 'json' do
post '', auth: [], provides: 'json' do
halt 400, "\"name\" is required" unless json_params["name"]
halt 400, "\"category\" is required" unless json_params["category"]
halt 400, "\"amount\" is required" unless json_params["amount"]
@ -67,7 +67,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
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
put '/:id', auth: [], provides: 'json' do
pex = LifePex::Pex.find(
id: params["id"],
user_id: current_user.id,
@ -88,7 +88,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
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
delete '/:id', auth: [], provides: 'json' do
pex = LifePex::Pex.find(
id: params["id"],
user_id: current_user.id,
@ -105,7 +105,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
produces 'application/json'
status_codes [200]
parameter :id, required: true, type: 'string', in: 'path'
get "/:id", auth: [], provides: 'json' do
get '/:id', auth: [], provides: 'json' do
pex = LifePex::Pex.find(
id: params["id"],
user_id: current_user.id,
@ -120,7 +120,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
produces 'application/json,text/html'
status_codes [200]
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(
id: params["id"],
user_id: current_user_id,
@ -152,27 +152,55 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
end
end
summary 'Get the amount of a pex each day since first occurence since 1 january'
notes ""
summary 'Get the amount of a pex each day since first occurence since a given date'
notes ''
produces 'application/json'
status_codes [200]
parameter :id, required: true, type: 'integer', in: 'path'
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"
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 = LifePex::Pex.find(id: pex_id, user_id: current_user_id)
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
year = Date.today.year
first_jan = Date.new(year, 1, 1)
user_pexs = LifePex::UserPex.where {
self.user_id == current_user_id && self.pex_id == pex_id && created_at > first_jan
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
first_jan = Date.new(year, 1, 1)
start_date = first_jan
end
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
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
include LifePex::Systems::ApiList
end

47
src/systems/recall.rb Normal file
View 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

11
src/utils/env.rb Normal file
View 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

View File

@ -38,6 +38,8 @@ html lang="en"
a.btn.btn-lg.btn-dark href="/recap" Recap
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
li.nav-item

View File

@ -28,7 +28,12 @@ tr.full-row(class=@row_class)
| <<
a.btn.btn-warning href="/pexs/update?id=#{pex_with_amount[:pex][:id]}"
| u
td.col-8=pex_with_amount[:pex][:name]
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

View File

@ -15,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 /
.form-group.row
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
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") /

View 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
View 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

View File

@ -2,6 +2,17 @@ script src="https://code.highcharts.com/highcharts.js"
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
span.badge.bg-primary
| Level #{level.current_level.round}
@ -10,6 +21,15 @@ h1 Recap
| #{level.xp_from_current_level.round} / #{level.xp_for_complete_level.round}
.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"] || "60") /
.form-group.row
p
input.btn.btn-lg.btn-block type="submit" value="Reload"
#recap-xp-container style="width:100%; height:400px;"
.recap-success

View File

@ -22,7 +22,7 @@ class UserSystemTest < LifePexTest
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"]
assert_match (/auth=[\-\.\w]+; domain=example.org; path=\//), last_response.headers["Set-Cookie"]
end
def test_register_refuse_duplicate