diff --git a/Gemfile b/Gemfile index 214f1b0..e1872d6 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index eb73ba5..80c448b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/images/pictograms/delete-outline.svg b/app/assets/images/pictograms/delete-outline.svg new file mode 100644 index 0000000..bcf8601 --- /dev/null +++ b/app/assets/images/pictograms/delete-outline.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/pictograms/plus-outline.svg b/app/assets/images/pictograms/plus-outline.svg new file mode 100644 index 0000000..dd4c86d --- /dev/null +++ b/app/assets/images/pictograms/plus-outline.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/pictograms/weight-kilogram.svg b/app/assets/images/pictograms/weight-kilogram.svg new file mode 100644 index 0000000..e7ebb64 --- /dev/null +++ b/app/assets/images/pictograms/weight-kilogram.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 7ff4439..db853e7 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -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; +} diff --git a/app/controllers/units_controller.rb b/app/controllers/units_controller.rb new file mode 100644 index 0000000..bbd5c9c --- /dev/null +++ b/app/controllers/units_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 503ce07..a8fc04f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -49,5 +49,4 @@ class UsersController < ApplicationController def find_user @user = User.find(params[:id]) end - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 998c020..f0845a7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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) diff --git a/app/models/unit.rb b/app/models/unit.rb new file mode 100644 index 0000000..4f50405 --- /dev/null +++ b/app/models/unit.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 9b0997b..e6275ef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb new file mode 100644 index 0000000..52a255f --- /dev/null +++ b/app/views/units/_form.html.erb @@ -0,0 +1,26 @@ +
<%= f.text_field :shortname, required: true, size: 20 %>
+<%= f.text_field :name, size: 60 %>
+<%= User.human_attribute_name(:symbol).capitalize %> | +<%= User.human_attribute_name(:name).capitalize %> | +<%= User.human_attribute_name(:multiplier).capitalize %> | + <% if current_user.at_least(:active) %> +<%= t :actions %> | + <% end %> +
---|---|---|---|
0 %>> + <%= link_to unit.symbol, edit_unit_path(unit) %> + | +<%= unit.name %> | +<%= unit.multiplier unless unit.multiplier == 1 %> | + <% if current_user.at_least(:active) %> ++ <%= image_button_to t(".delete_unit"), "delete-outline", unit_path(unit), + method: :delete %> + | + <% end %> +