diff --git a/.rubocop.yml b/.rubocop.yml index 803a6275..b389cd4c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,6 @@ inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.3 + Exclude: + - 'db/**/*' + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8d0b95ee..eeacf4ca 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -13,13 +13,13 @@ Metrics/AbcSize: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 200 + Max: 200 Exclude: - 'spec/**/*' - 'test/**/*' # "Line is too long"を無効 -Metrics/LineLength: +Layout/LineLength: Enabled: false # Offense count: 1 @@ -50,10 +50,3 @@ EndOfLine: Metrics/ModuleLength: Max: 120 - -# NOTE: Follow Redmine's model definition -Rails/ApplicationRecord: - Enabled: false - -Rails/InverseOf: - Enabled: false diff --git a/Dockerfile b/Dockerfile index 71b0a0e7..464c4432 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,15 @@ RUN apt-get install -qq -y \ sqlite3 default-libmysqlclient-dev RUN apt-get install -qq -y build-essential libc6-dev +# for e2e test env +RUN sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - +RUN apt-get update && apt-get install -y google-chrome-stable +RUN google-chrome --version | perl -pe 's/([^0-9]+)([0-9]+)(\.[0-9]+).+/$2/g' > chrome-version-major +RUN curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE_`cat chrome-version-major` > chrome-version +RUN curl -O -L http://chromedriver.storage.googleapis.com/`cat chrome-version`/chromedriver_linux64.zip && rm chrome-version* +RUN unzip chromedriver_linux64.zip && mv chromedriver /usr/local/bin + RUN cd /tmp && svn co http://svn.redmine.org/redmine/trunk redmine WORKDIR /tmp/redmine diff --git a/README.md b/README.md index ac8b9980..2b172253 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,18 @@ If you have any requests, bug reports, please use GitHub issues. e + logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" + {} + end + + def custom_fields_map_by_tracker_id(tracker_id = nil) + return {} unless builtin_fields_enabled? + return {} if tracker_id.blank? + + tracker = Tracker.find_by(id: tracker_id) + ids = tracker&.custom_field_ids || [] + fields = IssueCustomField.where(id: ids) + map = {} + fields.each do |field| + id = "issue_custom_field_values_#{field.id}" + attributes = field.attributes + + attributes = attributes.merge(possible_values: field.possible_values_options.map { |value| value[0] }) if field.format.name == 'bool' + map[id] = attributes + end + map + rescue StandardError => e + logger&.info "core_fields_map_by_tracker_id failed due to this error: #{e.message}" + {} + end end end diff --git a/app/controllers/concerns/project_templates_common.rb b/app/controllers/concerns/project_templates_common.rb index 7c1b7fb1..75da6f17 100644 --- a/app/controllers/concerns/project_templates_common.rb +++ b/app/controllers/concerns/project_templates_common.rb @@ -4,7 +4,7 @@ module Concerns module ProjectTemplatesCommon extend ActiveSupport::Concern included do - before_action :find_user, :find_project, :authorize, except: %i[preview load] + before_action :find_user, :find_project, :authorize, except: %i[preview load load_selectable_fields] before_action :find_object, only: %i[show edit update destroy] accept_api_auth :index, :list_templates, :load end diff --git a/app/controllers/global_issue_templates_controller.rb b/app/controllers/global_issue_templates_controller.rb index 11d4904d..68c39c35 100644 --- a/app/controllers/global_issue_templates_controller.rb +++ b/app/controllers/global_issue_templates_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # noinspection RubocopInspection class GlobalIssueTemplatesController < ApplicationController layout 'base' @@ -30,8 +32,16 @@ def new end def create - @global_issue_template = GlobalIssueTemplate.new(valid_params) + @global_issue_template = GlobalIssueTemplate.new @global_issue_template.author = User.current + + begin + @global_issue_template.safe_attributes = valid_params + rescue ActiveRecord::SerializationTypeMismatch, Concerns::IssueTemplatesCommon::InvalidTemplateFormatError + flash[:error] = I18n.t(:builtin_fields_should_be_valid_json, default: 'Please enter a valid JSON fotmat string.') + render render_form_params.merge(action: :new) + return + end save_and_flash(:notice_successful_create, :new) && return end @@ -40,7 +50,14 @@ def show end def update - @global_issue_template.safe_attributes = valid_params + begin + @global_issue_template.safe_attributes = valid_params + rescue ActiveRecord::SerializationTypeMismatch, Concerns::IssueTemplatesCommon::InvalidTemplateFormatError + flash[:error] = I18n.t(:builtin_fields_should_be_valid_json, default: 'Please enter a valid JSON fotmat string.') + render render_form_params.merge(action: :show) + return + end + save_and_flash(:notice_successful_update, :show) end @@ -48,7 +65,14 @@ def edit # Change from request.post to request.patch for Rails4. return unless request.patch? || request.put? - @global_issue_template.safe_attributes = valid_params + begin + @global_issue_template.safe_attributes = valid_params + rescue ActiveRecord::SerializationTypeMismatch + flash[:error] = I18n.t(:builtin_fields_should_be_valid_json, default: 'Please enter a valid JSON fotmat string.') + render render_form_params.merge(action: :show) + return + end + save_and_flash(:notice_successful_update, :show) end @@ -106,14 +130,20 @@ def save_and_flash(message, action_on_failure) def template_params params.require(:global_issue_template) .permit(:title, :tracker_id, :issue_title, :description, :note, :is_default, :enabled, - :author_id, :position, project_ids: [], checklists: []) + :author_id, :position, :related_link, :link_title, :builtin_fields, + project_ids: [], checklists: []) end def render_form_params trackers = Tracker.all projects = Project.all + tracker_id = @global_issue_template.tracker_id + custom_fields = core_fields_map_by_tracker_id(tracker_id: tracker_id) + .merge(custom_fields_map_by_tracker_id(tracker_id)).to_json + { layout: !request.xhr?, locals: { checklist_enabled: checklist_enabled?, trackers: trackers, apply_all_projects: apply_all_projects?, - issue_template: @global_issue_template, projects: projects } } + issue_template: @global_issue_template, projects: projects, custom_fields: custom_fields.to_s, + builtin_fields_enable: builtin_fields_enabled? } } end end diff --git a/app/controllers/issue_templates_controller.rb b/app/controllers/issue_templates_controller.rb index ef220463..1aa1bb88 100644 --- a/app/controllers/issue_templates_controller.rb +++ b/app/controllers/issue_templates_controller.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # noinspection ALL class IssueTemplatesController < ApplicationController layout 'base' @@ -49,15 +50,30 @@ def new end def create - @issue_template = IssueTemplate.new(valid_params) + @issue_template = IssueTemplate.new @issue_template.author = User.current @issue_template.project = @project + + begin + @issue_template.safe_attributes = valid_params + rescue ActiveRecord::SerializationTypeMismatch, Concerns::IssueTemplatesCommon::InvalidTemplateFormatError + flash[:error] = I18n.t(:builtin_fields_should_be_valid_json, default: 'Please enter a valid JSON fotmat string.') + render render_form_params.merge(action: :new) + return + end + # TODO: Should return validation error in case mandatory fields are blank. save_and_flash(:notice_successful_create, :new) && return end def update - @issue_template.safe_attributes = valid_params + begin + @issue_template.safe_attributes = valid_params + rescue ActiveRecord::SerializationTypeMismatch, Concerns::IssueTemplatesCommon::InvalidTemplateFormatError + flash[:error] = I18n.t(:builtin_fields_should_be_valid_json, default: 'Please enter a valid JSON fotmat string.') + render render_form_params.merge(action: :show) + return + end save_and_flash(:notice_successful_update, :show) end @@ -70,7 +86,9 @@ def load else IssueTemplate.find(issue_template_id) end - render plain: issue_template.template_json + rendered_json = builtin_fields_enabled? ? issue_template.template_json : issue_template.template_json(except: 'builtin_fields_json') + + render plain: rendered_json end # update pulldown @@ -194,7 +212,9 @@ def inherit_templates def template_params params.require(:issue_template).permit(:tracker_id, :title, :note, :issue_title, :description, :is_default, - :enabled, :author_id, :position, :enabled_sharing, checklists: []) + :enabled, :author_id, :position, :enabled_sharing, + :related_link, :link_title, :builtin_fields, + checklists: []) end def templates_exist? @@ -202,8 +222,12 @@ def templates_exist? end def render_form_params + child_project_used_count = template&.used_projects&.count + custom_fields = core_fields_map_by_tracker_id(tracker_id: template&.tracker_id, project_id: @project.id) + .merge(custom_fields_map_by_tracker_id(template&.tracker_id)).to_json + { layout: !request.xhr?, - locals: { issue_template: template, project: @project, - checklist_enabled: checklist_enabled? } } + locals: { issue_template: template, project: @project, child_project_used_count: child_project_used_count, + checklist_enabled: checklist_enabled?, custom_fields: custom_fields.to_s, builtin_fields_enable: builtin_fields_enabled? } } end end diff --git a/app/models/concerns/issue_template/common.rb b/app/models/concerns/issue_template/common.rb index 19159e65..efee22f3 100644 --- a/app/models/concerns/issue_template/common.rb +++ b/app/models/concerns/issue_template/common.rb @@ -17,6 +17,7 @@ module Common validates :title, presence: true validates :tracker, presence: true + validates :related_link, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true scope :enabled, -> { where(enabled: true) } scope :sorted, -> { order(:position) } @@ -41,6 +42,9 @@ module Common after_destroy do |template| logger.info("[Destroy] #{self.class}: #{template.inspect}") end + + # ActiveRecord::SerializationTypeMismatch may be thrown if non hash object is assigned. + serialize :builtin_fields_json, Hash end # @@ -64,14 +68,21 @@ def checklist end end - def template_json + def template_json(except: nil) template = {} template[self.class::Config::JSON_OBJECT_NAME] = generate_json - template.to_json(root: true) + return template.to_json(root: true) if except.blank? + + template.to_json(root: true, except: [except]) + end + + def builtin_fields + builtin_fields_json.to_json end def generate_json result = attributes + result[:link_title] = link_title.presence || I18n.t(:issue_template_related_link, default: 'Related Link') result[:checklist] = checklist result.except('checklist_json') end diff --git a/app/models/global_issue_template.rb b/app/models/global_issue_template.rb index eaadc4c6..b32357f8 100644 --- a/app/models/global_issue_template.rb +++ b/app/models/global_issue_template.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + class GlobalIssueTemplate < ActiveRecord::Base include Redmine::SafeAttributes include Concerns::IssueTemplate::Common - validates_uniqueness_of :title, scope: :tracker_id + validates :title, uniqueness: { scope: :tracker_id } has_and_belongs_to_many :projects - acts_as_positioned :scope => [:tracker_id] + acts_as_positioned scope: [:tracker_id] safe_attributes 'title', 'description', @@ -16,7 +18,10 @@ class GlobalIssueTemplate < ActiveRecord::Base 'project_ids', 'position', 'author_id', - 'checklist_json' + 'checklist_json', + 'related_link', + 'link_title', + 'builtin_fields_json' # for intermediate table assosciations scope :search_by_project, lambda { |project_id| @@ -24,7 +29,7 @@ class GlobalIssueTemplate < ActiveRecord::Base } module Config - JSON_OBJECT_NAME = 'global_issue_template'.freeze + JSON_OBJECT_NAME = 'global_issue_template' end Config.freeze diff --git a/app/models/issue_template.rb b/app/models/issue_template.rb index 42355dc5..a88be5b1 100644 --- a/app/models/issue_template.rb +++ b/app/models/issue_template.rb @@ -1,14 +1,28 @@ +# frozen_string_literal: true + class IssueTemplate < ActiveRecord::Base include Redmine::SafeAttributes include Concerns::IssueTemplate::Common belongs_to :project validates :project_id, presence: true - validates_uniqueness_of :title, scope: :project_id + validates :title, uniqueness: { scope: :project_id } acts_as_positioned scope: %i[project_id tracker_id] # author and project should be stable. - safe_attributes 'title', 'description', 'tracker_id', 'note', 'enabled', 'issue_title', 'is_default', - 'enabled_sharing', 'visible_children', 'position', 'checklist_json' + safe_attributes 'title', + 'description', + 'tracker_id', + 'note', + 'enabled', + 'issue_title', + 'is_default', + 'enabled_sharing', + 'visible_children', + 'position', + 'checklist_json', + 'related_link', + 'link_title', + 'builtin_fields_json' scope :enabled_sharing, -> { where(enabled_sharing: true) } scope :search_by_project, lambda { |prolect_id| @@ -16,7 +30,7 @@ class IssueTemplate < ActiveRecord::Base } module Config - JSON_OBJECT_NAME = 'issue_template'.freeze + JSON_OBJECT_NAME = 'issue_template' end Config.freeze diff --git a/app/views/common/_nodata.html.erb b/app/views/common/_nodata.html.erb index e0e0316b..eaaa378a 100644 --- a/app/views/common/_nodata.html.erb +++ b/app/views/common/_nodata.html.erb @@ -1,5 +1,5 @@ <% if trackers.blank? %> -
+
<%= simple_format(l(:text_no_tracker_enabled)) %>
<% end %> \ No newline at end of file diff --git a/app/views/common/_orphaned.html.erb b/app/views/common/_orphaned.html.erb index 68645a85..675ffc34 100644 --- a/app/views/common/_orphaned.html.erb +++ b/app/views/common/_orphaned.html.erb @@ -1,5 +1,5 @@

<%= l(:orphaned_template) %>

- +
@@ -12,7 +12,7 @@ <% orphaned_templates.each do |issue_template| %> - + issue_template issue'> - + - - + + <% end %> diff --git a/app/views/common/_template_links.html.erb b/app/views/common/_template_links.html.erb index bb679bb0..b93373df 100644 --- a/app/views/common/_template_links.html.erb +++ b/app/views/common/_template_links.html.erb @@ -1,6 +1,6 @@ -
-
#
<%= link_to h(issue_template.id), { controller: controller.controller_name, action: 'show', @@ -27,21 +27,21 @@ { title: "#{html_escape(issue_template.note) }"} %> -
- -
- <%= issue_template.title %> +
+ +
+ <%= issue_template.title %> <%= textilizable(issue_template.description) %>
<%= "ID: #{issue_template.tracker_id}" %> <%=h issue_template.author %><%= format_time(issue_template.updated_on)%> <%= format_time(issue_template.updated_on) %>
+
@@ -34,10 +34,10 @@ <% template_map[tracker].sorted.each do |issue_template| %> - + issue_template issue'> - - + + - @@ -71,12 +76,17 @@ <%= javascript_tag do %> - $(function() { $("table.table-sortable tbody").positionedItems(); }); + // NOTE: Sortable feature depends on Redmine's sorting jQuery plugin. + $(function() { $('table.table-sortable tbody').positionedItems() }) <% end %> <% end %> -<%= link_to l(:orphaned_templates, default: 'Orphaned Templates'), orphaned_templates_global_issue_templates_path, remote: true, - id: 'orphaned_template_link', class: 'orphaned_template_link collapsible collapsed template-help' -%> - + + + diff --git a/app/views/global_issue_templates/new.html.erb b/app/views/global_issue_templates/new.html.erb index 12e198ad..abd6c96f 100644 --- a/app/views/global_issue_templates/new.html.erb +++ b/app/views/global_issue_templates/new.html.erb @@ -1,8 +1,8 @@ -
+
<%= link_to(l(:label_list_templates), { controller: 'global_issue_templates', action: 'index' }, class: 'icon icon-template') %>
-

<%=h "#{l(:issue_templates)} / #{l(:button_add)}" %>

+

<%=h "#{l(:issue_templates)} / #{l(:button_add)}" %>

<%= labelled_form_for :global_issue_template, issue_template, url: { controller: 'global_issue_templates', action: 'create' }, @@ -11,5 +11,6 @@ <%= render 'form', { f: f, checklist_enabled: checklist_enabled, trackers: trackers, projects: projects, - issue_template: issue_template, apply_all_projects: apply_all_projects } %> + issue_template: issue_template, apply_all_projects: apply_all_projects, + custom_fields: custom_fields, builtin_fields_enable: builtin_fields_enable } %> <% end %> diff --git a/app/views/global_issue_templates/show.html.erb b/app/views/global_issue_templates/show.html.erb index be1f01bc..b36174cf 100644 --- a/app/views/global_issue_templates/show.html.erb +++ b/app/views/global_issue_templates/show.html.erb @@ -1,14 +1,14 @@ -
+
<%= link_to l(:button_delete), { controller: 'global_issue_templates', action: 'destroy', id: issue_template }, - data: { confirm: l(:text_are_you_sure)}, + data: { confirm: l(:text_are_you_sure) }, title: l(:enabled_template_cannot_destroy, default: 'Only disabled template can be destroyed.'), disabled: issue_template.enabled?, method: 'delete', class: 'icon icon-del template-disabled-link' %> <%= link_to(l(:label_list_templates), { controller: 'global_issue_templates', action: 'index' }, class: 'icon icon-template') %>
-

+

<%= l(:global_issue_templates) %>: #<%= issue_template.id %> <%= issue_template.title %> <%= avatar(issue_template.author, size: '24') %>

@@ -19,5 +19,6 @@ html: { id: 'global_issue_template-form', class: nil, multipart: false } do |f| %> <%= render 'form', { f: f, checklist_enabled: checklist_enabled, trackers: trackers, - issue_template: issue_template, projects: projects, apply_all_projects: apply_all_projects } %> + issue_template: issue_template, projects: projects, apply_all_projects: apply_all_projects, + custom_fields: custom_fields, builtin_fields_enable: builtin_fields_enable } %> <% end %> diff --git a/app/views/issue_templates/_form.html.erb b/app/views/issue_templates/_form.html.erb index 6cbebcc4..d03b9528 100644 --- a/app/views/issue_templates/_form.html.erb +++ b/app/views/issue_templates/_form.html.erb @@ -1,9 +1,9 @@ <%= error_messages_for 'issue_template' %> -
+

<%= f.text_field :title, required: true, size: 80, label: l(:issue_template_name) %>

-
- <%= l(:label_applied_for_issue) %> +
+ <%= l(:label_applied_for_issue) %>

<% if issue_template.tracker.blank? %> <%= f.select :tracker_id, template_target_trackers(project, issue_template), @@ -20,11 +20,11 @@ <% end %>

<%= f.text_field :issue_title, required: false, size: 80, label: l(:issue_title) %> - + <%= l(:help_for_this_field) %> - +

@@ -32,29 +32,102 @@ required: true, label: l(:issue_description), class: 'wiki-edit' %>

- <% if checklist_enabled %> -

+

- - - + +

    <% issue_template.checklist.each_with_index do |content, i| %> -
  • - +
  • + <%= content %> - + - +
  • <% end %>

<% end %> + + + + <% if builtin_fields_enable %> +
+

+ + + + <%= l(:help_for_this_field) %> + + +     

+

+ + + + + + + + + + + <%= l(:button_add) %> + +

+
+ <%= l(:label_field_information, default: 'Field information') %> +
{{ customFields[newItemTitle] }}
+
+
+
    +
  • + + {{ customFields[item.title].name }}: {{ item.value }} / {{ item.title }} + + + + <%= l(:unavailable_fields_for_this_tracker, default: 'Unavailable field for this tarcker') %> + : {{ item.value }} / {{ item.title }} + + + +
  • +
+ + <%= l(:button_reset) %> + <%= l(:button_apply) %> +
+

+
+ +

+ <%= f.text_area :builtin_fields, + required: false, cols: 60, rows: 4, + label: l(:label_builtin_fields_json, default: 'JSON for fields') %> +

+ + <% end %> + +

<%= f.text_area :note, cols: 70, rows: 3, @@ -62,31 +135,52 @@ label: l(:issue_template_note) %>

+

<%= f.text_field :related_link, type: 'url', + size: 70, label: l(:issue_template_related_link, default: 'Related link') %> + + <%= l(:help_for_this_field) %> + + +

+ + +

<%= f.check_box :is_default, label: l(:field_is_default) %> - + <%= l(:help_for_this_field) %> - +

<%= f.check_box :enabled, label: l(:label_enabled) %> - + <%= l(:help_for_this_field) %> - +

<%= f.check_box :enabled_sharing, label: l(:label_enabled_sharing) %> - + <%= l(:help_for_this_field) %> - +

@@ -95,66 +189,201 @@ <%= wikitoolbar_for 'issue_template_description' %> -
#
<%= link_to h(issue_template.id), { controller: 'global_issue_templates', id: issue_template.id, action: 'show' }, - { title: issue_template.title} %> + { title: issue_template.title } %> <%= link_to h(issue_template.title), { controller: 'global_issue_templates', @@ -45,11 +45,16 @@ { title: "#{html_escape(issue_template.note)}" } %> -
- -
- <%= issue_template.title %> +
+ +
+ <%= issue_template.title %> <%= textilizable(issue_template.description) %> + <% if issue_template.related_link.present? %> +
+ <%= link_to issue_template.link_title.present? ? issue_template.link_title : l(:issue_template_related_link, default: 'Related link'), + issue_template.related_link, target: '_blank', rel: 'nofollow noopener', class: 'external' %> + <% end %>
@@ -57,10 +62,10 @@
<%=h issue_template.tracker.name %> <%=h issue_template.author %> <%= format_time(issue_template.updated_on)%> <%= checked_image issue_template.is_default? %><%= checked_image issue_template.enabled? %><%= checked_image issue_template.is_default? %><%= checked_image issue_template.enabled? %> + <%= reorder_handle(issue_template, :url => url_for({ controller: 'global_issue_templates', id: issue_template.id, action: 'update' })) %>
+
@@ -9,7 +9,7 @@ <% issue_templates.each do |template| %> - + template_data'> @@ -17,18 +17,23 @@ <%= template.issue_title %> - + <% end %> @@ -41,23 +46,28 @@ <%= template.issue_title %> - + <% end %> <% global_issue_templates.each do |template| %> - + template_data'> @@ -65,25 +75,34 @@ <%= template.issue_title %> - + <% end %>
<%=h l(:issue_template_name) %>
<%= template.title %> -
- -
- <%= template.title %> +
+ +
+ <%= template.title %> <%= textilizable(template.description) %> + <% if template.related_link.present? %> +
+ <%= link_to template.link_title.present? ? template.link_title : l(:issue_template_related_link, default: 'Related link'), + template.related_link, target: '_blank', rel: 'nofollow noopener', class: 'external' %> + <% end %>
<%= checked_image template.is_default? %><%= checked_image template.is_default? %> - +
-
- -
+
+ +
<%= template.title %> <%= textilizable(template.description) %> + <% if template.related_link.present? %> +
+ <%= link_to template.link_title.present? ? template.link_title : l(:issue_template_related_link, default: 'Related link'), + template.related_link, target: '_blank', rel: 'nofollow noopener', class: 'external' %> + <% end %>
<%= checked_image template == default_template %><%= checked_image template == default_template %> - +
<%= template.title %> -
- -
- <%= template.issue_title %> +
+ +
+ <%= template.issue_title %> <%= textilizable(template.description) %> + <% if template.related_link.present? %> +
+ <%= link_to template.link_title.present? ? template.link_title : l(:issue_template_related_link, default: 'Related link'), + template.related_link, target: '_blank', rel: 'nofollow noopener', class: 'external' %> + <% end %>
<%= checked_image template == default_template %><%= checked_image template == default_template %> - +
+ diff --git a/app/views/issue_templates/_note_form.html.erb b/app/views/issue_templates/_note_form.html.erb index 67135f66..06f6b3b6 100644 --- a/app/views/issue_templates/_note_form.html.erb +++ b/app/views/issue_templates/_note_form.html.erb @@ -6,19 +6,22 @@ project_id = issue&.project_id tracker_id = issue&.tracker_id %> -
-