Add Units

This commit is contained in:
cryptogopher 2023-07-06 18:34:16 +02:00
parent 6f415dfb62
commit a4745c9cb8
21 changed files with 285 additions and 15 deletions

View File

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

View File

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

View 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

View 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

View 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

View File

@ -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;
}

View 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

View File

@ -49,5 +49,4 @@ class UsersController < ApplicationController
def find_user
@user = User.find(params[:id])
end
end

View File

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

View File

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

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

View File

@ -0,0 +1 @@
/var/www/app/views/units/new.html.erb

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

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

View File

@ -0,0 +1 @@
require 'core_ext/big_decimal/formatting'

View File

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

View File

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

View 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

View File

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

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