forked from fixin.me/fixin.me
Add Units
This commit is contained in:
parent
6f415dfb62
commit
a4745c9cb8
2
Gemfile
2
Gemfile
@ -10,6 +10,8 @@ gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
|
||||
|
||||
gem "devise"
|
||||
|
||||
gem 'awesome_nested_set'
|
||||
|
||||
group :development, :test do
|
||||
gem "byebug"
|
||||
end
|
||||
|
@ -68,6 +68,8 @@ GEM
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
awesome_nested_set (3.5.0)
|
||||
activerecord (>= 4.0.0, < 7.1)
|
||||
bcrypt (3.1.18)
|
||||
bindex (0.8.1)
|
||||
builder (3.2.4)
|
||||
@ -210,6 +212,7 @@ PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
awesome_nested_set
|
||||
byebug
|
||||
capybara
|
||||
devise
|
||||
|
1
app/assets/images/pictograms/delete-outline.svg
Normal file
1
app/assets/images/pictograms/delete-outline.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" /></svg>
|
After Width: | Height: | Size: 189 B |
1
app/assets/images/pictograms/plus-outline.svg
Normal file
1
app/assets/images/pictograms/plus-outline.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M4,9H9V4H15V9H20V15H15V20H9V15H4V9M11,13V18H13V13H18V11H13V6H11V11H6V13H11Z" /></svg>
|
After Width: | Height: | Size: 165 B |
1
app/assets/images/pictograms/weight-kilogram.svg
Normal file
1
app/assets/images/pictograms/weight-kilogram.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M12,3A4,4 0 0,1 16,7C16,7.73 15.81,8.41 15.46,9H18C18.95,9 19.75,9.67 19.95,10.56C21.96,18.57 22,18.78 22,19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19C2,18.78 2.04,18.57 4.05,10.56C4.25,9.67 5.05,9 6,9H8.54C8.19,8.41 8,7.73 8,7A4,4 0 0,1 12,3M12,5A2,2 0 0,0 10,7A2,2 0 0,0 12,9A2,2 0 0,0 14,7A2,2 0 0,0 12,5M6,11V19H8V16.5L9,17.5V19H11V17L9,15L11,13V11H9V12.5L8,13.5V11H6M15,11C13.89,11 13,11.89 13,13V17C13,18.11 13.89,19 15,19H18V14H16V17H15V13H18V11H15Z" /></svg>
|
After Width: | Height: | Size: 537 B |
@ -102,13 +102,17 @@ input:read-only:hover {
|
||||
.nav-menu .left > * {
|
||||
float: left;
|
||||
}
|
||||
/* TODO: inactive tab color #d0d0d0 or #c7c7c7 */
|
||||
.nav-menu .tab {
|
||||
border: none;
|
||||
border-bottom: solid 0.2rem #a0a0a0;
|
||||
border-radius: 0;
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0.8rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
.nav-menu .tab:hover {
|
||||
border-bottom: solid 0.2rem #009ade;
|
||||
}
|
||||
.nav-menu .tab.active {
|
||||
border-bottom: solid 0.2rem;
|
||||
color: #009ade;
|
||||
@ -276,20 +280,23 @@ table.items tbody tr:hover {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
table.items th,
|
||||
table.items td:not(:first-child):not(.actions) {
|
||||
table.items td {
|
||||
padding: 0 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
table.items td {
|
||||
border-top: 1px solid #dddddd;
|
||||
}
|
||||
table.items td:first-child {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
table.items a {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
line-height: 2.5rem;
|
||||
line-height: 2.2rem;
|
||||
padding: 0 0.8rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
@ -318,11 +325,15 @@ table.items svg {
|
||||
vertical-align: middle;
|
||||
width: 1.2rem;
|
||||
}
|
||||
table.items td.number {
|
||||
text-align: right;
|
||||
}
|
||||
table.items td.actions {
|
||||
padding-left: 0.8rem;
|
||||
padding: 0 0 0 0.8rem;
|
||||
text-align: right;
|
||||
}
|
||||
table.items button {
|
||||
font-weight: normal;
|
||||
margin-right: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
@ -335,3 +346,8 @@ table.items select:focus-within,
|
||||
table.items select:focus-visible {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.contextual {
|
||||
float: right;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
57
app/controllers/units_controller.rb
Normal file
57
app/controllers/units_controller.rb
Normal file
@ -0,0 +1,57 @@
|
||||
class UnitsController < ApplicationController
|
||||
before_action :find_unit, only: [:edit, :update, :destroy]
|
||||
|
||||
before_action except: :index do
|
||||
raise AccessForbidden unless current_user.at_least(:active)
|
||||
end
|
||||
before_action only: [:edit, :update, :destroy] do
|
||||
raise ArgumentError unless current_user == @unit.user
|
||||
end
|
||||
|
||||
def index
|
||||
@units = current_user.units
|
||||
end
|
||||
|
||||
def new
|
||||
@unit = current_user.units.new
|
||||
end
|
||||
|
||||
def create
|
||||
@unit = current_user.units.new(unit_params)
|
||||
if @unit.save
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to units_url
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @unit.update(unit_params)
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to units_url
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @unit.destroy
|
||||
flash[:notice] = t(".success")
|
||||
end
|
||||
redirect_to units_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unit_params
|
||||
params.require(:unit).permit(:symbol, :name, :base_id, :multiplier)
|
||||
end
|
||||
|
||||
def find_unit
|
||||
@unit = Unit.find(params[:id])
|
||||
end
|
||||
end
|
@ -49,5 +49,4 @@ class UsersController < ApplicationController
|
||||
def find_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -69,21 +69,27 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def navigation_menu
|
||||
#menu_items = {right: [[:users, :index],]}
|
||||
menu_items = {right: [
|
||||
[".users", "account-multiple-outline", users_path, :admin],
|
||||
[".units", "weight-kilogram", units_path, :restricted],
|
||||
]}
|
||||
|
||||
content_tag :div, class: "right" do
|
||||
if current_user.at_least(:admin)
|
||||
image_link_to t(".users"), "account-multiple-outline", users_path, class: "tab",
|
||||
current: :active
|
||||
menu_items.map do |alignment, items|
|
||||
content_tag :div, class: alignment do
|
||||
items.map do |label, image, path, status|
|
||||
if current_user.at_least(status)
|
||||
image_link_to t(label), image, path, class: "tab", current: :active
|
||||
end
|
||||
end.join.html_safe
|
||||
end
|
||||
end
|
||||
end.join.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def image_element_to(type, name, image = nil, options = nil, html_options = {})
|
||||
current = html_options.delete(:current)
|
||||
return "" if (current == :hide) && (url_for(options) == request.path)
|
||||
current = (url_for(options) == request.path) && html_options.delete(:current)
|
||||
return "" if current == :hide
|
||||
|
||||
name = svg_tag("pictograms/#{image}") + name if image
|
||||
html_options[:class] = class_names(html_options[:class], "button", active: current == :active)
|
||||
|
26
app/models/unit.rb
Normal file
26
app/models/unit.rb
Normal file
@ -0,0 +1,26 @@
|
||||
class Unit < ApplicationRecord
|
||||
attribute :multiplier, default: 1
|
||||
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :base, optional: true, class_name: "Unit"
|
||||
|
||||
validates :symbol, presence: true, uniqueness: {scope: :user_id}
|
||||
validates :multiplier, numericality: {equal_to: 1}, unless: :base
|
||||
validates :multiplier, numericality: {other_than: 1}, if: :base
|
||||
validate if: -> { base.present? } do
|
||||
errors.add(:base, :only_top_level_base_units) unless base.base.nil?
|
||||
end
|
||||
|
||||
acts_as_nested_set parent_column: :base_id, scope: :user, dependent: :destroy, order_column: :multiplier
|
||||
|
||||
scope :defaults, -> { where(user: nil) }
|
||||
|
||||
after_save if: :base do |record|
|
||||
record.move_to_ordered_child_of(record.base, :multiplier)
|
||||
end
|
||||
|
||||
before_destroy do
|
||||
# TODO: disallow destruction if any object depends on this unit
|
||||
nil
|
||||
end
|
||||
end
|
@ -11,6 +11,8 @@ class User < ApplicationRecord
|
||||
disabled: 0, # administratively disallowed to sign in
|
||||
}, default: :active
|
||||
|
||||
has_many :units, -> { order :lft }, dependent: :destroy
|
||||
|
||||
def at_least(status)
|
||||
User.statuses[self.status] >= User.statuses[status]
|
||||
end
|
||||
|
26
app/views/units/_form.html.erb
Normal file
26
app/views/units/_form.html.erb
Normal file
@ -0,0 +1,26 @@
|
||||
<div id="add-unit" <%= 'style=display:none;' if @unit.errors.empty? %>>
|
||||
<h2><%= t ".heading_new_unit" %></h2>
|
||||
|
||||
<%= labelled_form_for @unit,
|
||||
url: project_units_path(@project),
|
||||
html: {id: 'unit-add-form', name: 'unit-add-form'} do |f| %>
|
||||
|
||||
<%= render partial: 'units/form', locals: {f: f} %>
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<%= link_to l(:button_cancel), "#", onclick: '$("#add-unit").hide(); return false;' %>
|
||||
<% end %>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<%= error_messages_for @unit %>
|
||||
|
||||
<div class="box tabular">
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
<p><%= f.text_field :shortname, required: true, size: 20 %></p>
|
||||
</div>
|
||||
<div class="splitcontentright">
|
||||
<p><%= f.text_field :name, size: 60 %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
1
app/views/units/edit.html.erb
Symbolic link
1
app/views/units/edit.html.erb
Symbolic link
@ -0,0 +1 @@
|
||||
/var/www/app/views/units/new.html.erb
|
35
app/views/units/index.html.erb
Normal file
35
app/views/units/index.html.erb
Normal file
@ -0,0 +1,35 @@
|
||||
<div class="contextual">
|
||||
<% if current_user.at_least(:active) %>
|
||||
<%= image_link_to t(".add_unit"), "plus-outline", new_unit_path %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<table class="items" id="units">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= User.human_attribute_name(:symbol).capitalize %></th>
|
||||
<th><%= User.human_attribute_name(:name).capitalize %></th>
|
||||
<th><%= User.human_attribute_name(:multiplier).capitalize %></th>
|
||||
<% if current_user.at_least(:active) %>
|
||||
<th><%= t :actions %></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% Unit.each_with_level(@units) do |unit, level| %>
|
||||
<tr>
|
||||
<td <%= "style=padding-left:0.5rem;" if level > 0 %>>
|
||||
<%= link_to unit.symbol, edit_unit_path(unit) %>
|
||||
</td>
|
||||
<td><%= unit.name %></td>
|
||||
<td class="number"><%= unit.multiplier unless unit.multiplier == 1 %></td>
|
||||
<% if current_user.at_least(:active) %>
|
||||
<td class="actions">
|
||||
<%= image_button_to t(".delete_unit"), "delete-outline", unit_path(unit),
|
||||
method: :delete %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
22
app/views/units/new.html.erb
Normal file
22
app/views/units/new.html.erb
Normal file
@ -0,0 +1,22 @@
|
||||
<% content_for :navigation, flush: true do %>
|
||||
<div class="left">
|
||||
<%= image_link_to t(:back), "arrow-left-bold-outline",
|
||||
request.referer.present? ? :back : units_url %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= tabular_form_for @unit do |f| %>
|
||||
<%= f.text_field :symbol, required: true, size: 10, autofocus: true, autocomplete: "off" %>
|
||||
<%= f.text_field :name, size: 25, autocomplete: "off" %>
|
||||
|
||||
<% if current_user.units.roots.count %>
|
||||
<%= f.select :base_id,
|
||||
current_user.units.roots.collect { |u| ["#{u.symbol}#{' - ' + u.name if u.name}", u.id] },
|
||||
{include_blank: t(".none")},
|
||||
onchange: 'this.form.unit_multiplier.disabled = (this.value == "");' %>
|
||||
<%= f.number_field :multiplier, step: "any", disabled: @unit.base.nil?, size: 10,
|
||||
autocomplete: "off" %>
|
||||
<% end %>
|
||||
|
||||
<%= f.submit @unit.persisted? ? t(:update) : t(:add) %>
|
||||
<% end %>
|
1
config/initializers/core_ext.rb
Normal file
1
config/initializers/core_ext.rb
Normal file
@ -0,0 +1 @@
|
||||
require 'core_ext/big_decimal/formatting'
|
@ -1,6 +1,11 @@
|
||||
en:
|
||||
activerecord:
|
||||
attributes:
|
||||
unit:
|
||||
symbol: Symbol
|
||||
name: Name
|
||||
multiplier: Multiplier
|
||||
base: Base unit
|
||||
user:
|
||||
email: e-mail
|
||||
status: status
|
||||
@ -8,6 +13,18 @@ en:
|
||||
created_at: registered
|
||||
confirmed_at: confirmed
|
||||
unconfirmed_email: Awaiting confirmation for
|
||||
units:
|
||||
index:
|
||||
add_unit: Add unit
|
||||
delete_unit: Delete
|
||||
new:
|
||||
none: none
|
||||
create:
|
||||
success: Created new unit
|
||||
update:
|
||||
success: Updated unit
|
||||
destroy:
|
||||
success: Deleted unit
|
||||
users:
|
||||
index:
|
||||
disguise: View as...
|
||||
@ -36,11 +53,14 @@ en:
|
||||
application:
|
||||
revert: Revert
|
||||
sign_out: Sign out
|
||||
units: Units
|
||||
users: Users
|
||||
actions: Actions
|
||||
add: Add
|
||||
back: Back
|
||||
or: or
|
||||
register: Register
|
||||
sign_in: Sign in
|
||||
recover_password: Recover password
|
||||
resend_confirmation: Resend confirmation
|
||||
update: Update
|
||||
|
@ -2,6 +2,8 @@ Rails.application.routes.draw do
|
||||
devise_for :users, path: '', path_names: {registration: 'profile'},
|
||||
controllers: {registrations: :registrations}
|
||||
|
||||
resources :units, except: [:show]
|
||||
|
||||
resources :users, only: [:index, :show, :update] do
|
||||
member do
|
||||
post :disguise
|
||||
|
19
db/migrate/20230602185352_create_units.rb
Normal file
19
db/migrate/20230602185352_create_units.rb
Normal file
@ -0,0 +1,19 @@
|
||||
class CreateUnits < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :units do |t|
|
||||
t.references :user, foreign_key: true
|
||||
t.string :symbol
|
||||
t.string :name
|
||||
t.decimal :multiplier, precision: 30, scale: 15
|
||||
|
||||
t.references :base
|
||||
t.integer :lft, null: false
|
||||
t.integer :rgt, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :units, [:user_id, :symbol], unique: true
|
||||
add_index :units, :lft
|
||||
add_index :units, :rgt
|
||||
end
|
||||
end
|
22
db/schema.rb
22
db/schema.rb
@ -10,7 +10,24 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_03_11_220654) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_06_02_185352) do
|
||||
create_table "units", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "user_id"
|
||||
t.string "symbol"
|
||||
t.string "name"
|
||||
t.decimal "multiplier", precision: 30, scale: 15
|
||||
t.bigint "base_id"
|
||||
t.integer "lft", null: false
|
||||
t.integer "rgt", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["base_id"], name: "index_units_on_base_id"
|
||||
t.index ["lft"], name: "index_units_on_lft"
|
||||
t.index ["rgt"], name: "index_units_on_rgt"
|
||||
t.index ["user_id", "symbol"], name: "index_units_on_user_id_and_symbol", unique: true
|
||||
t.index ["user_id"], name: "index_units_on_user_id"
|
||||
end
|
||||
|
||||
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "email", limit: 64, null: false
|
||||
t.integer "status", default: 0, null: false
|
||||
@ -23,10 +40,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_11_220654) do
|
||||
t.string "confirmation_token"
|
||||
t.datetime "confirmed_at"
|
||||
t.datetime "confirmation_sent_at"
|
||||
t.string "unconfirmed_email"
|
||||
t.string "unconfirmed_email", limit: 64
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "units", "users"
|
||||
end
|
||||
|
12
lib/core_ext/big_decimal/formatting.rb
Normal file
12
lib/core_ext/big_decimal/formatting.rb
Normal file
@ -0,0 +1,12 @@
|
||||
#require "bigdecimal"
|
||||
#require "bigdecimal/util"
|
||||
|
||||
module FixinMe
|
||||
module BigDecimalWithGrouping
|
||||
def to_s(format = "3F")
|
||||
super(format)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BigDecimal.prepend(FixinMe::BigDecimalWithGrouping)
|
Loading…
x
Reference in New Issue
Block a user