From eb619184a546718b3339ad7bcf532f6a3f4b477f Mon Sep 17 00:00:00 2001
From: artoscinote <85488244+artoscinote@users.noreply.github.com>
Date: Mon, 8 Aug 2022 10:06:00 +0200
Subject: [PATCH] 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]
---
.../images/label_template_icons/fluics.svg | 6 ++
.../label_templates_datatable.js | 2 +-
app/controllers/label_templates_controller.rb | 11 +---
app/datatables/label_template_datatable.rb | 22 +++++--
app/models/fluics_label_template.rb | 32 +++++++++
app/models/label_template.rb | 31 +++++----
app/models/team.rb | 7 ++
app/models/zebra_label_template.rb | 12 ++++
app/serializers/label_template_serializer.rb | 4 ++
.../label_templates/repository_row_service.rb | 50 ++++++++++++++
config/initializers/extends.rb | 11 +++-
config/locales/en.yml | 8 +++
.../20220530144300_fix_label_template.rb | 4 +-
.../20220803122405_update_label_templates.rb | 30 +++++++++
db/structure.sql | 65 ++++++++++++++++---
15 files changed, 254 insertions(+), 41 deletions(-)
create mode 100644 app/assets/images/label_template_icons/fluics.svg
create mode 100644 app/models/fluics_label_template.rb
create mode 100644 app/models/zebra_label_template.rb
create mode 100644 app/services/label_templates/repository_row_service.rb
create mode 100644 db/migrate/20220803122405_update_label_templates.rb
diff --git a/app/assets/images/label_template_icons/fluics.svg b/app/assets/images/label_template_icons/fluics.svg
new file mode 100644
index 000000000..2f19db0b3
--- /dev/null
+++ b/app/assets/images/label_template_icons/fluics.svg
@@ -0,0 +1,6 @@
+
diff --git a/app/assets/javascripts/label_templates/label_templates_datatable.js b/app/assets/javascripts/label_templates/label_templates_datatable.js
index 1446ef747..23f376f7e 100644
--- a/app/assets/javascripts/label_templates/label_templates_datatable.js
+++ b/app/assets/javascripts/label_templates/label_templates_datatable.js
@@ -23,7 +23,7 @@
}
function renderNameHTML(data, type, row) {
- return `${data.icon_url} 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)
)
diff --git a/app/models/fluics_label_template.rb b/app/models/fluics_label_template.rb
new file mode 100644
index 000000000..64a39c48e
--- /dev/null
+++ b/app/models/fluics_label_template.rb
@@ -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
diff --git a/app/models/label_template.rb b/app/models/label_template.rb
index e2b140c29..7a0c1bd9f 100644
--- a/app/models/label_template.rb
+++ b/app/models/label_template.rb
@@ -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
diff --git a/app/models/team.rb b/app/models/team.rb
index 749af729e..9f338ff45 100644
--- a/app/models/team.rb
+++ b/app/models/team.rb
@@ -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
diff --git a/app/models/zebra_label_template.rb b/app/models/zebra_label_template.rb
new file mode 100644
index 000000000..5dbdc290d
--- /dev/null
+++ b/app/models/zebra_label_template.rb
@@ -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
diff --git a/app/serializers/label_template_serializer.rb b/app/serializers/label_template_serializer.rb
index 6b4ee7b27..279952655 100644
--- a/app/serializers/label_template_serializer.rb
+++ b/app/serializers/label_template_serializer.rb
@@ -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)
diff --git a/app/services/label_templates/repository_row_service.rb b/app/services/label_templates/repository_row_service.rb
new file mode 100644
index 000000000..de762d1b7
--- /dev/null
+++ b/app/services/label_templates/repository_row_service.rb
@@ -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
diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb
index feb043a9a..d4e56b406 100644
--- a/config/initializers/extends.rb
+++ b/config/initializers/extends.rb
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8b7ba68ef..847d82009 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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'
diff --git a/db/migrate/20220530144300_fix_label_template.rb b/db/migrate/20220530144300_fix_label_template.rb
index 720e2bf8f..e447db1f8 100644
--- a/db/migrate/20220530144300_fix_label_template.rb
+++ b/db/migrate/20220530144300_fix_label_template.rb
@@ -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
diff --git a/db/migrate/20220803122405_update_label_templates.rb b/db/migrate/20220803122405_update_label_templates.rb
new file mode 100644
index 000000000..132847a3b
--- /dev/null
+++ b/db/migrate/20220803122405_update_label_templates.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index ea7f62b76..173ce0980 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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');