Implement typed label templates [SCI-7046] (#4316)

* Implement typed label templates [SCI-7046]

* Add structure.sql, fix migration [SCI-7046]

* Fix method name [SCI-7046]

* Add dependent option to team - label_template relation [SCI-7046]

* Migration code style, add input sanitization [SCI-7046]

* Fix new label creation [SCI-7046]
This commit is contained in:
artoscinote 2022-08-08 10:06:00 +02:00 committed by GitHub
parent e5448f3d1f
commit eb619184a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 254 additions and 41 deletions

View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="#FFE000"/>
<path d="M11.6466 3.72125L14.5012 8.00001L11.6466 12.2788L11.6461 12.2795C11.6008 12.3476 11.5394 12.4034 11.4673 12.4418C11.3953 12.4803 11.3148 12.5003 11.233 12.5H11.2312H2C1.86739 12.5 1.74021 12.4473 1.64645 12.3536C1.55268 12.2598 1.5 12.1326 1.5 12V4.00001C1.5 3.8674 1.55268 3.74022 1.64645 3.64645C1.74021 3.55268 1.86739 3.50001 2 3.50001L11.2312 3.50001L11.233 3.5C11.3148 3.49971 11.3953 3.5197 11.4673 3.55818C11.5394 3.59666 11.6008 3.65243 11.6461 3.72048L11.6466 3.72125Z" stroke="#00A4CE"/>
<circle cx="12" cy="8" r="1" fill="#00A4CE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 5H6H9V6H6V8H8V9H6V11H5V5Z" fill="#363332"/>
</svg>

After

Width:  |  Height:  |  Size: 806 B

View file

@ -23,7 +23,7 @@
}
function renderNameHTML(data, type, row) {
return `${data.icon_url}<a
return `${data.icon_image_tag}<a
href='${row.DT_RowAttr['data-edit-url']}'
class='record-info-link'
onclick='window.open(this.href, "_self")'

View file

@ -33,14 +33,7 @@ class LabelTemplatesController < ApplicationController
end
def create
label_template = LabelTemplate.create!(
team_id: current_team.id,
name: I18n.t('label_templates.new_label_template'),
language_type: :zpl,
format: 'ZPL',
size: '1" x 0.5" / 25.4mm x 12.7mm',
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
)
label_template = ZebraLabelTemplate.default.save!
redirect_to label_template_path(label_template, new_label: true)
end
@ -82,7 +75,7 @@ class LabelTemplatesController < ApplicationController
def set_default
ActiveRecord::Base.transaction do
LabelTemplate.find_by(team_id: current_team.id,
language_type: @label_template.language_type,
type: @label_template.type,
default: true)&.update!(default: false)
@label_template.update!(default: true)
render json: { message: I18n.t('label_templates.index.template_set_as_default') }

View file

@ -35,8 +35,8 @@ class LabelTemplateDatatable < CustomDatatable
{
'0' => record.id,
'1' => record.default,
'2' => append_format_icon(sanitize_input(record.name)),
'3' => sanitize_input(record.format),
'2' => append_format_icon(record),
'3' => sanitize_input(record.label_format),
'4' => sanitize_input(record.description),
'5' => sanitize_input(record.modified_by),
'6' => I18n.l(record.updated_at, format: :full),
@ -52,9 +52,15 @@ class LabelTemplateDatatable < CustomDatatable
end
end
def append_format_icon(data)
{ icon_url: ActionController::Base.helpers.image_tag('label_template_icons/zpl.svg', class: 'label-template-icon'),
name: data }
def append_format_icon(record)
{
icon_image_tag:
ActionController::Base.helpers.image_tag(
"label_template_icons/#{record.icon}.svg",
class: 'label-template-icon'
),
name: sanitize_input(record.name)
}
end
def get_raw_records
@ -67,12 +73,16 @@ class LabelTemplateDatatable < CustomDatatable
).select('label_templates.* AS label_templates')
.select('creators.full_name AS created_by_user')
.select('modifiers.full_name AS modified_by')
.select(
"('#{Extends::LABEL_TEMPLATE_FORMAT_MAP.to_json}'::jsonb -> label_templates.type)::text "\
"AS label_format"
)
LabelTemplate.from(res, :label_templates)
end
def filter_records(records)
records.where_attributes_like(
['label_templates.name', 'label_templates.format', 'label_templates.description',
['label_templates.name', 'label_templates.label_format', 'label_templates.description',
'label_templates.modified_by', 'label_templates.created_by_user'],
dt_params.dig(:search, :value)
)

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class FluicsLabelTemplate < LabelTemplate
def self.default
FluicsLabelTemplate.new(
name: I18n.t('label_templates.default_fluics_name'),
width_mm: 25.4,
height_mm: 12.7,
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
)
end
def label_format
'Fluics'
end
def created_by_user
'Fluics GmbH'
end
def modified_by
'Fluics GmbH'
end
def icon
'fluics'
end
def read_only?
true
end
end

View file

@ -3,13 +3,13 @@
class LabelTemplate < ApplicationRecord
include SearchableModel
enum language_type: { zpl: 0 }
belongs_to :team
validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH,
maximum: Constants::NAME_MAX_LENGTH }
validates :size, presence: true
validates :content, presence: true
validate :default_template
validate :ensure_single_default_template!
def self.enabled?
ApplicationSettings.instance.values['label_templates_enabled']
@ -21,18 +21,27 @@ class LabelTemplate < ApplicationRecord
end
end
def icon_url
case language_type
when 'zpl'
'/images/label_template_icons/zpl.svg'
end
def icon
'zpl'
end
def language_type
'zpl'
end
def read_only?
false
end
def label_format
Extends::LABEL_TEMPLATE_FORMAT_MAP[type]
end
private
def default_template
if default && LabelTemplate.where(team_id: team_id, default: true, language_type: language_type)
.where.not(id: id).any?
def ensure_single_default_template!
if default && self.class.where(team_id: team_id, default: true, language_type: language_type)
.where.not(id: id).any?
errors.add(:default, I18n.t('activerecord.errors.models.label_template.attributes.default.already_exist'))
end
end

View file

@ -11,6 +11,7 @@ class Team < ApplicationRecord
include ActionView::Helpers::NumberHelper
after_create :generate_template_project
after_create :create_default_label_templates
scope :teams_select, -> { select(:id, :name).order(name: :asc) }
scope :ordered, -> { order('LOWER(name)') }
@ -41,6 +42,7 @@ class Team < ApplicationRecord
has_many :assets, inverse_of: :team, dependent: :destroy
has_many :team_repositories, inverse_of: :team, dependent: :destroy
has_many :shared_repositories, through: :team_repositories, source: :repository
has_many :label_templates, dependent: :destroy
attr_accessor :without_templates
@ -170,4 +172,9 @@ class Team < ApplicationRecord
return if without_templates
TemplatesService.new.delay(queue: :templates).update_team(self)
end
def create_default_label_templates
ZebraLabelTemplate.default.update(team: self, default: true)
FluicsLabelTemplate.default.update(team: self, default: true)
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class ZebraLabelTemplate < LabelTemplate
def self.default
ZebraLabelTemplate.new(
name: I18n.t('label_templates.default_zebra_name'),
width_mm: 25.4,
height_mm: 12.7,
content: Extends::DEFAULT_LABEL_TEMPLATE[:zpl]
)
end
end

View file

@ -5,6 +5,10 @@ class LabelTemplateSerializer < ActiveModel::Serializer
attributes :name, :description, :language_type, :icon_url, :urls, :content
def icon_url
ActionController::Base.helpers.image_path("label_template_icons/#{object.icon}.svg")
end
def urls
{
update: label_template_path(object)

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
module LabelTemplates
class RepositoryRowService
class UnsupportedKeyError < StandardError; end
class ColumnNotFoundError < StandardError; end
def initialize(label_template, repository_row)
@label_template = label_template
@repository_row = repository_row
end
def render
keys = @label_template.content.scan(/(?<=\{\{).*?(?=\}\})/).uniq
keys.reduce(@label_template.content.dup) do |rendered_content, key|
rendered_content.gsub!(/\{\{#{key}\}\}/, fetch_value(key))
end
end
private
def fetch_value(key)
case key
when /^COLUMN_\[(.*)\]/
name = Regexp.last_match(1)
repository_cell = @repository_row.repository_cells.joins(:repository_column).find_by(
repository_columns: { name: name }
)
unless repository_cell
raise UnsupportedKeyError, I18n.t('label_templates.repository_row.errors.column_not_found', column: name)
end
repository_cell.value.formatted
when 'ITEM_ID'
@repository_row.code
when 'NAME'
@repository_row.name
when 'ADDED_BY'
@repository_row.created_by.full_name
when 'ADDED_ON'
@repository_row.created_at.to_s
else
raise UnsupportedKeyError, I18n.t('label_templates.repository_row.errors.unsupported_key', key: key)
end
end
end
end

View file

@ -502,12 +502,17 @@ class Extends
^LH20,20
^PW310
^CF0,23
^FO0,0^FD{{item_id}}^FS
^FO0,20^BQN,2,4^FDMA,{{item_id}}^FS
^FO95,30^FB180,4,0,L^FD{{item_name}}^FS^FS
^FO0,0^FD{{ITEM_ID}}^FS
^FO0,20^BQN,2,4^FDMA,{{ITEM_ID}}^FS
^FO95,30^FB180,4,0,L^FD{{NAME}}^FS^FS
^XZ
HEREDOC
}
LABEL_TEMPLATE_FORMAT_MAP = {
'ZebraLabelTemplate' => 'ZPL',
'FluicsLabelTemplate' => 'Fluics'
}
end
# rubocop:enable Style/MutableConstant

View file

@ -802,6 +802,12 @@ en:
normal_user: "Normal user"
admin: "Administrator"
label_templates:
repository_row:
errors:
unsupported_key: "Key %{key} is not supported by this label template renderer."
column_not_found: "Column '%{column}' not found on inventory item."
user_projects:
view_users:
modal_title: "Members of %{name}"
@ -815,6 +821,8 @@ en:
can_add_user_to_project: "Can not add user to the project."
label_templates:
default_zebra_name: 'SciNote Item (ZPL)'
default_fluics_name: 'SciNote Item (Fluics)'
new_label_template: 'New label'
index:
head_title: 'Label templates'

View file

@ -2,7 +2,8 @@
class FixLabelTemplate < ActiveRecord::Migration[6.1]
def change
LabelTemplate.last.update(
# rubocop:disable Rails/SkipsModelValidations
LabelTemplate.last.update_columns(
name: 'SciNote Item',
size: '1" x 0.5" / 25.4mm x 12.7mm',
language_type: :zpl,
@ -23,5 +24,6 @@ class FixLabelTemplate < ActiveRecord::Migration[6.1]
^XZ
HEREDOC
)
# rubocop:enable Rails/SkipsModelValidations
end
end

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class UpdateLabelTemplates < ActiveRecord::Migration[6.1]
def up
change_table :label_templates, bulk: true do |t|
t.string :type
t.float :width_mm
t.float :height_mm
t.remove :format
t.remove :language_type
t.remove :size
end
# Remove our original default template
LabelTemplate.order(created_at: :asc).find_by(default: true)&.destroy
Team.find_each { |t| t.__send__(:create_default_label_templates) }
end
def down
change_table :label_templates, bulk: true do |t|
t.remove :type
t.remove :width_mm
t.remoe :height_mm
t.string :format, null: false, default: 'ZPL'
t.integer :language_type
t.string :size
end
end
end

View file

@ -698,11 +698,16 @@ CREATE TABLE public.label_templates (
id bigint NOT NULL,
name character varying NOT NULL,
content text NOT NULL,
language_type integer,
"default" boolean DEFAULT false NOT NULL,
size character varying,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
updated_at timestamp(6) without time zone NOT NULL,
description character varying,
last_modified_by_id integer,
created_by_id integer,
team_id bigint,
type character varying,
width_mm double precision,
height_mm double precision
);
@ -5165,10 +5170,24 @@ CREATE INDEX index_hidden_repository_cell_reminders_on_user_id ON public.hidden_
--
-- Name: index_label_templates_on_language_type; Type: INDEX; Schema: public; Owner: -
-- Name: index_label_templates_on_created_by_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_label_templates_on_language_type ON public.label_templates USING btree (language_type);
CREATE INDEX index_label_templates_on_created_by_id ON public.label_templates USING btree (created_by_id);
--
-- Name: index_label_templates_on_last_modified_by_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_label_templates_on_last_modified_by_id ON public.label_templates USING btree (last_modified_by_id);
--
-- Name: index_label_templates_on_team_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_label_templates_on_team_id ON public.label_templates USING btree (team_id);
--
@ -5816,10 +5835,10 @@ CREATE INDEX index_repository_cells_on_repository_row_id ON public.repository_ce
--
-- Name: index_repository_cells_on_value; Type: INDEX; Schema: public; Owner: -
-- Name: index_repository_cells_on_value_type_and_value_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_repository_cells_on_value ON public.repository_cells USING btree (value_type, value_id);
CREATE INDEX index_repository_cells_on_value_type_and_value_id ON public.repository_cells USING btree (value_type, value_id);
--
@ -6845,10 +6864,10 @@ CREATE INDEX index_view_states_on_user_id ON public.view_states USING btree (use
--
-- Name: index_view_states_on_viewable; Type: INDEX; Schema: public; Owner: -
-- Name: index_view_states_on_viewable_type_and_viewable_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_view_states_on_viewable ON public.view_states USING btree (viewable_type, viewable_id);
CREATE INDEX index_view_states_on_viewable_type_and_viewable_id ON public.view_states USING btree (viewable_type, viewable_id);
--
@ -6943,6 +6962,14 @@ ALTER TABLE ONLY public.assets
ADD CONSTRAINT fk_rails_0916329f9e FOREIGN KEY (created_by_id) REFERENCES public.users(id);
--
-- Name: label_templates fk_rails_09d7cc0c34; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.label_templates
ADD CONSTRAINT fk_rails_09d7cc0c34 FOREIGN KEY (team_id) REFERENCES public.teams(id);
--
-- Name: user_assignments fk_rails_0b13c65ab0; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -7007,6 +7034,14 @@ ALTER TABLE ONLY public.user_assignments
ADD CONSTRAINT fk_rails_19dca62dfc FOREIGN KEY (user_role_id) REFERENCES public.user_roles(id);
--
-- Name: label_templates fk_rails_1aa41d1093; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.label_templates
ADD CONSTRAINT fk_rails_1aa41d1093 FOREIGN KEY (created_by_id) REFERENCES public.users(id);
--
-- Name: user_system_notifications fk_rails_20d9487a3c; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -8087,6 +8122,14 @@ ALTER TABLE ONLY public.experiments
ADD CONSTRAINT fk_rails_d683124fa4 FOREIGN KEY (restored_by_id) REFERENCES public.users(id);
--
-- Name: label_templates fk_rails_d6ac71e421; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.label_templates
ADD CONSTRAINT fk_rails_d6ac71e421 FOREIGN KEY (last_modified_by_id) REFERENCES public.users(id);
--
-- Name: protocols fk_rails_d8007e2f63; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -8516,6 +8559,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220429083335'),
('20220530144300'),
('20220602120714'),
('20220705091621');
('20220705091621'),
('20220726133419'),
('20220803122405');