diff options
22 files changed, 1036 insertions, 43 deletions
@@ -161,6 +161,7 @@ group :test do gem 'cucumber-rails', require: false gem 'simplecov', :require => false gem 'simplecov-rcov', :require => false + gem 'htmlbeautifier' end group :test, :development, :dev do diff --git a/Gemfile.lock b/Gemfile.lock index e87e6bb3b..eea6f4ba5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,6 +261,7 @@ GEM hashdiff (0.3.2) highline (1.7.8) hike (1.2.3) + htmlbeautifier (1.3.1) httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.8.1) @@ -604,6 +605,7 @@ DEPENDENCIES georuby-ext (= 0.0.5) google-analytics-rails has_array_of! + htmlbeautifier i18n-tasks inherited_resources jbuilder (~> 2.0) diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index 437444f29..e95670241 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -24,19 +24,21 @@ class ReferentialsController < BreadcrumbController end def show - resource.switch - show! do |format| - format.json { - render :json => { :lines_count => resource.lines.count, - :networks_count => resource.networks.count, - :vehicle_journeys_count => resource.vehicle_journeys.count + resource.vehicle_journey_frequencies.count, - :time_tables_count => resource.time_tables.count, - :referential_id => resource.id} - } - format.html { build_breadcrumb :show} - end - - @reflines = lines_collection.paginate(page: params[:page], per_page: 10) + resource.switch + show! do |format| + @referential = @referential.decorate + + format.json { + render :json => { :lines_count => resource.lines.count, + :networks_count => resource.networks.count, + :vehicle_journeys_count => resource.vehicle_journeys.count + resource.vehicle_journey_frequencies.count, + :time_tables_count => resource.time_tables.count, + :referential_id => resource.id} + } + format.html { build_breadcrumb :show} + end + + @reflines = lines_collection.paginate(page: params[:page], per_page: 10) end def edit diff --git a/app/controllers/workbenches_controller.rb b/app/controllers/workbenches_controller.rb index ccd55965b..1447c27de 100644 --- a/app/controllers/workbenches_controller.rb +++ b/app/controllers/workbenches_controller.rb @@ -14,6 +14,10 @@ class WorkbenchesController < BreadcrumbController q_for_result = scope.ransack(params[:q].merge(archived_at_not_null: nil, archived_at_null: nil)) @wbench_refs = sort_result(q_for_result.result).paginate(page: params[:page], per_page: 30) + @wbench_refs = ModelDecorator.decorate( + @wbench_refs, + with: ReferentialDecorator + ) @q = scope.ransack(params[:q]) show! do diff --git a/app/decorators/company_decorator.rb b/app/decorators/company_decorator.rb index 3a0cc16ce..5a2fdd1c0 100644 --- a/app/decorators/company_decorator.rb +++ b/app/decorators/company_decorator.rb @@ -1,4 +1,6 @@ class CompanyDecorator < Draper::Decorator + decorates Chouette::Company + delegate_all def self.collection_decorator_class @@ -9,4 +11,33 @@ class CompanyDecorator < Draper::Decorator object.lines.count end + def action_links + links = [] + + if h.policy(Chouette::Company).create? + links << Link.new( + name: h.t('companies.actions.new'), + href: h.new_line_referential_company_path(@line_referential) + ) + end + + if h.policy(object).update? + links << Link.new( + name: h.t('companies.actions.edit'), + href: h.edit_line_referential_company_path(@line_referential, object) + ) + end + + if h.policy(object).destroy? + links << Link.new( + name: t('companies.actions.destroy'), + href: h.line_referential_company_path(@line_referential, object), + method: :delete, + data: { confirm: t('companies.actions.destroy_confirm') } + ) + end + + links + end + end diff --git a/app/decorators/model_decorator.rb b/app/decorators/model_decorator.rb new file mode 100644 index 000000000..dee014cc3 --- /dev/null +++ b/app/decorators/model_decorator.rb @@ -0,0 +1,3 @@ +class ModelDecorator < PaginatingDecorator + delegate :model +end diff --git a/app/decorators/referential_decorator.rb b/app/decorators/referential_decorator.rb new file mode 100644 index 000000000..5112f9dd4 --- /dev/null +++ b/app/decorators/referential_decorator.rb @@ -0,0 +1,48 @@ +class ReferentialDecorator < Draper::Decorator + delegate_all + + def action_links + links = [ + Link.new( + name: h.t('time_tables.index.title'), + href: h.referential_time_tables_path(object) + ) + ] + + if h.policy(object).clone? + links << Link.new( + name: h.t('actions.clone'), + href: h.new_referential_path(from: object.id) + ) + end + + if h.policy(object).edit? + # TODO: Handle buttons in the header and don't show them in the gear menu + # button.btn.btn-primary type='button' data-toggle='modal' data-target='#purgeModal' Purger + + if object.archived? + links << Link.new( + name: h.t('actions.unarchive'), + href: h.unarchive_referential_path(object.id), + method: :put + ) + else + links << Link.new( + name: h.t('actions.archive'), + href: h.archive_referential_path(object.id), + method: :put + ) + end + end + + if h.policy(object).destroy? + links << Link.new( + href: h.referential_path(object), + method: :delete, + data: { confirm: h.t('referentials.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/helpers/newapplication_helper.rb b/app/helpers/newapplication_helper.rb index 3d43e9fc7..27b9cb0a4 100644 --- a/app/helpers/newapplication_helper.rb +++ b/app/helpers/newapplication_helper.rb @@ -1,6 +1,7 @@ module NewapplicationHelper # Table Builder + # selectable means actions-for-selection def table_builder collection, columns, actions, selectable = [], cls = nil return unless collection.present? @@ -8,6 +9,7 @@ module NewapplicationHelper content_tag :tr do hcont = [] + # Adds checkbox to table header unless selectable.empty? cbx = content_tag :div, '', class: 'checkbox' do check_box_tag('0', 'all').concat(content_tag(:label, '', for: '0')) @@ -16,12 +18,14 @@ module NewapplicationHelper end columns.map do |k, v| + # These columns are hard-coded to not be sortable if ["ID Codif", "Oid", "OiD", "ID Reflex", "Arrêt de départ", "Arrêt d'arrivée", "Période de validité englobante", "Période englobante", "Nombre de courses associées", "Journées d'application", "Arrêts de l'itinéraire", "Arrêts inclus dans l'ITL"].include? k hcont << content_tag(:th, k) else hcont << content_tag(:th, sortable_columns(collection, k)) end end + # Inserts a blank column for the gear menu hcont << content_tag(:th, '') if actions.any? hcont.join.html_safe @@ -34,6 +38,8 @@ module NewapplicationHelper content_tag :tr do bcont = [] + # Adds item checkboxes whose value = the row object's id + # Apparently the object id is also used in the HTML id attribute without any prefix unless selectable.empty? cbx = content_tag :div, '', class: 'checkbox' do check_box_tag(item.try(:id), item.try(:id)).concat(content_tag(:label, '', for: item.try(:id))) @@ -48,9 +54,14 @@ module NewapplicationHelper else item.try(attribute) end + # if so this column's contents get transformed into a link to the object if attribute == 'name' or attribute == 'comment' lnk = [] + # #is_a? ? ; or ? + # TODO: Ask Jean-Paul: on which pages do we create multiple links? + # Do we actually create multiple links with this code? + # Answer: this is a polymorphic URL unless item.class == Calendar or item.class == Referential if current_referential lnk << current_referential @@ -172,6 +183,7 @@ module NewapplicationHelper pic2 = content_tag :span, '', class: "fa fa-sort-desc #{(direction == 'asc') ? 'active' : ''}" pics = content_tag :span, pic1 + pic2, class: 'orderers' + # This snake cases and downcases the class name. Should use the ActiveSupport method to do this obj = collection.model.to_s.gsub('Chouette::', '').scan(/[A-Z][a-z]+/).join('_').downcase (I18n.t("activerecord.attributes.#{obj}.#{key}") + pics).html_safe diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb new file mode 100644 index 000000000..4537a0795 --- /dev/null +++ b/app/helpers/table_builder_helper.rb @@ -0,0 +1,236 @@ +require 'table_builder_helper/column' +require 'table_builder_helper/custom_links' +require 'table_builder_helper/url' + +# TODO: Add doc comment about neeeding to make a decorator for your collections +# TODO: Document global variables this uses +module TableBuilderHelper + # TODO: rename this after migration from `table_builder` + def table_builder_2( + collection, + columns, + current_referential: nil, + + # When false, no columns will be sortable + sortable: true, + + selectable: false, + # selection_actions: [] ## this has been gotten rid of. The element based on this should be created elsewhere + links: [], # links: or actions: ? I think 'links' is better since 'actions' evokes Rails controller actions and we want to put `link_to`s here + # TODO: get rid of this argument. Going with params instead + sort_by: {}, # { column: 'name', direction: 'desc' } + cls: '' # can we rename this to "class"? + # ^^ rename to html_options = {} at the end of the non-keyword arguments? Hrm, not a fan of combining hash args and keyword args +# sort column +# sort direction + +# TODO: add `linked_column` or some such attribute that defines which column should be linked and what method to call to get it + ) + + content_tag :table, + thead(collection, columns, sortable, selectable, links.any?) + + tbody(collection, columns, selectable, links), + class: cls + end + + private + + def thead(collection, columns, sortable, selectable, has_links) + content_tag :thead do + content_tag :tr do + hcont = [] + + if selectable + hcont << content_tag(:th, checkbox(id_name: '0', value: 'all')) + end + + columns.each do |column| + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + )) + end + + # Inserts a blank column for the gear menu + hcont << content_tag(:th, '') if has_links + + hcont.join.html_safe + end + end + end + + def tbody(collection, columns, selectable, links) + content_tag :tbody do + collection.map do |item| + + content_tag :tr do + bcont = [] + + if selectable + bcont << content_tag( + :td, + checkbox(id_name: item.try(:id), value: item.try(:id)) + ) + end + + columns.each do |column| + value = column.value(item) + + if column_is_linkable?(column) + # Build a link to the `item` + polymorph_url = URL.polymorphic_url_parts(item) + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir') + else + bcont << content_tag(:td, value) + end + end + + if links.any? + bcont << content_tag( + :td, + build_links(item, links), + class: 'actions' + ) + end + + bcont.join.html_safe + end + end.join.html_safe + end + end + + def build_links(item, links) + trigger = content_tag :div, class: 'btn dropdown-toggle', data: { toggle: 'dropdown' } do + content_tag :span, '', class: 'fa fa-cog' + end + + menu = content_tag :ul, class: 'dropdown-menu' do + ( + CustomLinks.new(item, pundit_user, links).links + + item.action_links + ).map do |link| + # TODO: ensure the Delete link is formatted correctly with the spacer, + # icon, and label + content_tag :li, link_to( + link.name, + link.href, + method: link.method, + data: link.data + ) + end.join.html_safe + + # actions.map do |action| + # polymorph_url = [] + # + # unless [:show, :delete].include? action + # polymorph_url << action + # end + # + # polymorph_url += polymorphic_url_parts(item) + # + # if action == :delete + # if policy(item).present? + # if policy(item).destroy? + # # TODO: This tag is exactly the same as the one below it + # content_tag :li, '', class: 'delete-action' do + # link_to(polymorph_url, method: :delete, data: { confirm: 'Etes-vous sûr(e) de vouloir effectuer cette action ?' }) do + # txt = t("actions.#{action}") + # pic = content_tag :span, '', class: 'fa fa-trash' + # pic + txt + # end + # end + # end + # else + # content_tag :li, '', class: 'delete-action' do + # link_to(polymorph_url, method: :delete, data: { confirm: 'Etes-vous sûr(e) de vouloir effectuer cette action ?' }) do + # txt = t("actions.#{action}") + # pic = content_tag :span, '', class: 'fa fa-trash' + # pic + txt + # end + # end + # end + # + # elsif action == :edit + # if policy(item).present? + # if policy(item).update? + # content_tag :li, link_to(t("actions.#{action}"), polymorph_url) + # end + # else + # content_tag :li, link_to(t("actions.#{action}"), polymorph_url) + # end + # elsif action == :archive + # unless item.archived? + # content_tag :li, link_to(t("actions.#{action}"), polymorph_url, method: :put) + # end + # elsif action == :unarchive + # if item.archived? + # content_tag :li, link_to(t("actions.#{action}"), polymorph_url, method: :put) + # end + # else + # content_tag :li, link_to(t("actions.#{action}"), polymorph_url) + # end + # end.join.html_safe + end + + content_tag :div, trigger + menu, class: 'btn-group' + + end + + def build_column_header( + column, + table_is_sortable, + collection_model, + params, + sort_on, + sort_direction + ) + if !table_is_sortable + return column.header_label(collection_model) + end + + return column.name if !column.sortable + + direction = + if column.key.to_s == sort_on && sort_direction == 'desc' + 'asc' + else + 'desc' + end + + link_to(params.merge({direction: direction, sort: column.key})) do + arrow_up = content_tag( + :span, + '', + class: "fa fa-sort-asc #{direction == 'desc' ? 'active' : ''}" + ) + arrow_down = content_tag( + :span, + '', + class: "fa fa-sort-desc #{direction == 'asc' ? 'active' : ''}" + ) + + arrow_icons = content_tag :span, arrow_up + arrow_down, class: 'orderers' + + ( + column.header_label(collection_model) + + arrow_icons + ).html_safe + end + end + + def checkbox(id_name:, value:) + content_tag :div, '', class: 'checkbox' do + check_box_tag(id_name, value).concat( + content_tag(:label, '', for: id_name) + ) + end + end + + def column_is_linkable?(column) + column.attribute == 'name' || column.attribute == 'comment' + end +end diff --git a/app/helpers/table_builder_helper/column.rb b/app/helpers/table_builder_helper/column.rb new file mode 100644 index 000000000..b2fdf2b73 --- /dev/null +++ b/app/helpers/table_builder_helper/column.rb @@ -0,0 +1,36 @@ +module TableBuilderHelper + class Column + attr_reader :key, :name, :attribute, :sortable + + def initialize(key: nil, name: '', attribute:, sortable: true) + if key.nil? && name.empty? + raise ColumnMustHaveKeyOrNameError + end + + @key = key + @name = name + @attribute = attribute + @sortable = sortable + end + + def value(obj) + if @attribute.is_a?(Proc) + @attribute.call(obj) + else + obj.try(@attribute) + end + end + + def header_label(model = nil) + return @name unless name.empty? + + # Transform `Chouette::Line` into "line" + model_key = model.to_s.demodulize.underscore + + I18n.t("activerecord.attributes.#{model_key}.#{@key}") + end + end + + + class ColumnMustHaveKeyOrNameError < StandardError; end +end diff --git a/app/helpers/table_builder_helper/custom_links.rb b/app/helpers/table_builder_helper/custom_links.rb new file mode 100644 index 000000000..f03acac1e --- /dev/null +++ b/app/helpers/table_builder_helper/custom_links.rb @@ -0,0 +1,77 @@ +require 'table_builder_helper/url' + +module TableBuilderHelper + class CustomLinks + ACTIONS_TO_HTTP_METHODS = { + delete: :delete, + archive: :put, + unarchive: :put + } + + def initialize(obj, user_context, actions) + @obj = obj + @user_context = user_context + @actions = actions + end + + def links + actions_after_policy_check.map do |action| + Link.new( + name: I18n.t("actions.#{action}"), + href: polymorphic_url(action), + method: method_for_action(action) + ) + end + end + + def polymorphic_url(action) + polymorph_url = [] + + unless [:show, :delete].include?(action) + polymorph_url << action + end + + polymorph_url += URL.polymorphic_url_parts(@obj) + end + + def method_for_action(action) + ACTIONS_TO_HTTP_METHODS[action] + end + + def actions_after_policy_check + @actions.select do |action| + # Has policy and can destroy + (action == :delete && + Pundit.policy(@user_context, @obj).present? && + Pundit.policy(@user_context, @obj).destroy?) || + + # Doesn't have policy + (action == :delete && + !Pundit.policy(@user_context, @obj).present?) || + + # Has policy and can update + (action == :edit && + Pundit.policy(@user_context, @obj).present? && + Pundit.policy(@user_context, @obj).update?) || + + # Doesn't have policy + (action == :edit && + !Pundit.policy(@user_context, @obj).present?) || + + # Object isn't archived + (action == :archive && !@obj.archived?) || + + # Object is archived + (action == :unarchive && @obj.archived?) || + + action_is_allowed_regardless_of_policy(action) + end + end + + private + + def action_is_allowed_regardless_of_policy(action) + ![:delete, :edit, :archive, :unarchive].include?(action) + end + end +end diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb new file mode 100644 index 000000000..59099ee99 --- /dev/null +++ b/app/helpers/table_builder_helper/url.rb @@ -0,0 +1,24 @@ +module TableBuilderHelper + class URL + def self.polymorphic_url_parts(item) + polymorph_url = [] + + unless item.is_a?(Calendar) || item.is_a?(Referential) + if current_referential + polymorph_url << current_referential + polymorph_url << item.line if item.respond_to? :line + polymorph_url << item.route.line if item.is_a?(Chouette::RoutingConstraintZone) + polymorph_url << item if item.respond_to? :line_referential + polymorph_url << item.stop_area if item.respond_to? :stop_area + polymorph_url << item if item.respond_to? :stop_points || item.is_a?(Chouette::TimeTable) + elsif item.respond_to? :referential + polymorph_url << item.referential + end + else + polymorph_url << item + end + + polymorph_url + end + end +end diff --git a/app/views/referential_lines/show.html.slim b/app/views/referential_lines/show.html.slim index db99381d3..5fb1bb125 100644 --- a/app/views/referential_lines/show.html.slim +++ b/app/views/referential_lines/show.html.slim @@ -7,6 +7,7 @@ / Below is secundary actions & optional contents .row .col-lg-12.text-right.mb-sm + / TODO: Make a list of Link objects that can be rendered with link_tos here and in the gear menu = link_to @line.human_attribute_name(:footnotes), referential_line_footnotes_path(@referential, @line), class: 'btn btn-primary' = link_to t('routing_constraint_zones.index.title'), referential_line_routing_constraint_zones_path(@referential, @line), class: 'btn btn-primary' diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index 3c1e36302..1bbf350b4 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -8,23 +8,38 @@ / Below is secondary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - = link_to t('time_tables.index.title'), referential_time_tables_path(@referential), class: 'btn btn-primary' - - - if policy(@referential).clone? - = link_to t('actions.clone'), new_referential_path(from: @referential.id), class: 'btn btn-primary' - - - if policy(@referential).edit? - button.btn.btn-primary type='button' data-toggle='modal' data-target='#purgeModal' Purger - - - if @referential.archived? - = link_to t('actions.unarchive'), unarchive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' + /= link_to t('time_tables.index.title'), referential_time_tables_path(@referential), class: 'btn btn-primary' + / + /- if policy(@referential).clone? + / = link_to t('actions.clone'), new_referential_path(from: @referential.id), class: 'btn btn-primary' + / + /- if policy(@referential).edit? + / button.btn.btn-primary type='button' data-toggle='modal' data-target='#purgeModal' Purger + / + / - if @referential.archived? + / = link_to t('actions.unarchive'), unarchive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' + / - else + / = link_to t('actions.archive'), archive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' + / + /- if policy(@referential).destroy? + / = link_to referential_path(@referential), method: :delete, data: {confirm: t('referentials.actions.destroy_confirm')}, class: 'btn btn-primary' do + / span.fa.fa-trash + / span = t('actions.destroy') + /= leslinks.map { |l| link_to l.href, l.label } + - @referential.action_links.each do |link| + - if link.method == :delete + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + span.fa.fa-trash + span = t('actions.destroy') - else - = link_to t('actions.archive'), archive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' - - - if policy(@referential).destroy? - = link_to referential_path(@referential), method: :delete, data: {confirm: t('referentials.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + = link_to link.name, + link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' / PageContent .page_content diff --git a/app/views/workbenches/_filters.html.slim b/app/views/workbenches/_filters.html.slim index 7c5055963..0aedbdd62 100644 --- a/app/views/workbenches/_filters.html.slim +++ b/app/views/workbenches/_filters.html.slim @@ -12,7 +12,7 @@ = f.input :associated_lines_id_eq, as: :select, collection: @workbench.lines.includes(:company).order(:name), input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez une ligne...' }, label: false, label_method: :display_name, wrapper_html: { class: 'select2ed'} .form-group.togglable - = f.label @wbench_refs.human_attribute_name(:status), required: false, class: 'control-label' + = f.label Referential.human_attribute_name(:status), required: false, class: 'control-label' .form-group.checkbox_list = f.input :archived_at_not_null, label: ("<span>Conservé<span class='fa fa-archive'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } = f.input :archived_at_null, label: ("<span>En préparation<span class='sb sb-lg sb-preparing'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } @@ -22,7 +22,7 @@ = f.input :organisation_name_eq_any, collection: Organisation.order('name').pluck(:name), as: :check_boxes, label: false, label_method: lambda{|w| ("<span>#{w}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .form-group.togglable - = f.label @wbench_refs.human_attribute_name(:validity_period), required: false, class: 'control-label' + = f.label Referential.human_attribute_name(:validity_period), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :validity_period do |p| = p.input :begin_gteq, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date filter_menu-item' }, default: @begin_range, include_blank: @begin_range ? false : true diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim index 77e670923..6dec68f7a 100644 --- a/app/views/workbenches/show.html.slim +++ b/app/views/workbenches/show.html.slim @@ -22,18 +22,45 @@ - if @wbench_refs.any? .row .col-lg-12 - = table_builder @wbench_refs, - { :name => 'name', - :status => Proc.new {|w| w.archived? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe}, - :organisation => Proc.new {|w| w.organisation.name}, - :validity_period => Proc.new {|w| w.validity_period.nil? ? '-' : t('validity_range', debut: l(w.try(:validity_period).try(:begin), format: :short), end: l(w.try(:validity_period).try(:end), format: :short))}, - :lines => Proc.new {|w| w.lines.count}, - :created_at => Proc.new {|w| l(w.created_at, format: :short)}, - :updated_at => Proc.new {|w| l(w.updated_at, format: :short)}, - :published_at => ''}, - [:show, :edit, :archive, :unarchive, :delete], - [:delete], - 'table has-filter has-search' + = table_builder_2 @wbench_refs, + [ \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :status, \ + attribute: Proc.new {|w| w.archived? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :organisation, \ + attribute: Proc.new {|w| w.organisation.name} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :validity_period, \ + attribute: Proc.new {|w| w.validity_period.nil? ? '-' : t('validity_range', debut: l(w.try(:validity_period).try(:begin), format: :short), end: l(w.try(:validity_period).try(:end), format: :short))} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :lines, \ + attribute: Proc.new {|w| w.lines.count} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :created_at, \ + attribute: Proc.new {|w| l(w.created_at, format: :short)} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :updated_at, \ + attribute: Proc.new {|w| l(w.updated_at, format: :short)} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :published_at, \ + attribute: '' \ + ) \ + ], + selectable: true, + links: [:show, :edit], + cls: 'table has-filter has-search' + / [:delete], = new_pagination @wbench_refs, 'pull-right' diff --git a/lib/link.rb b/lib/link.rb new file mode 100644 index 000000000..c875b85ac --- /dev/null +++ b/lib/link.rb @@ -0,0 +1,10 @@ +class Link + attr_reader :name, :href, :method, :data + + def initialize(name: nil, href:, method: nil, data: nil) + @name = name + @href = href + @method = method + @data = data + end +end diff --git a/spec/factories/chouette_2_factories.rb b/spec/factories/chouette_2_factories.rb index e8eba13e6..a8e80702d 100644 --- a/spec/factories/chouette_2_factories.rb +++ b/spec/factories/chouette_2_factories.rb @@ -1,3 +1,4 @@ +# TODO: Move these factories into their own files so all factory definitions are consistent FactoryGirl.define do factory :organisation do diff --git a/spec/factories/workbenches.rb b/spec/factories/workbenches.rb index f51e7d94c..154482213 100644 --- a/spec/factories/workbenches.rb +++ b/spec/factories/workbenches.rb @@ -5,5 +5,24 @@ FactoryGirl.define do association :organisation, :factory => :organisation association :line_referential association :stop_area_referential + + trait :with_referential do + # TODO: change all => to : + # association :referential, + # organisation: { organisation } + + # after(:stub) do |workbench, evaluator| + # + # end + + referentials do |workbench| + [association( + :referential, + organisation: workbench.organisation, + line_referential: workbench.line_referential, + stop_area_referential: workbench.stop_area_referential + )] + end + end end end diff --git a/spec/helpers/table_builder_helper/column_spec.rb b/spec/helpers/table_builder_helper/column_spec.rb new file mode 100644 index 000000000..0f27703b2 --- /dev/null +++ b/spec/helpers/table_builder_helper/column_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe TableBuilderHelper::Column do + describe "#header_label" do + it "returns the column @name if present" do + expect( + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: nil + ).header_label + ).to eq('ID Codif') + end + + it "returns the I18n translation of @key if @name not present" do + expect( + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ).header_label(Chouette::Company) + ).to eq('Numéro de téléphone') + end + end +end diff --git a/spec/helpers/table_builder_helper/custom_links_spec.rb b/spec/helpers/table_builder_helper/custom_links_spec.rb new file mode 100644 index 000000000..b64e97527 --- /dev/null +++ b/spec/helpers/table_builder_helper/custom_links_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe TableBuilderHelper::CustomLinks do + describe "#actions_after_policy_check" do + it "includes :show" do + referential = build_stubbed(:referential) + user_context = UserContext.new( + build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'boiv:read-offer' + ] + ), + referential: referential + ) + + expect( + TableBuilderHelper::CustomLinks.new( + referential, + user_context, + [:show] + ).actions_after_policy_check + ).to eq([:show]) + end + end +end diff --git a/spec/helpers/table_builder_helper_spec.rb b/spec/helpers/table_builder_helper_spec.rb new file mode 100644 index 000000000..c006e33c0 --- /dev/null +++ b/spec/helpers/table_builder_helper_spec.rb @@ -0,0 +1,394 @@ +require 'spec_helper' +require 'htmlbeautifier' + +module TableBuilderHelper + include Pundit +end + +describe TableBuilderHelper, type: :helper do + describe "#table_builder_2" do + it "builds a table" do + referential = build_stubbed(:referential) + workbench = referential.workbench + + # user_context = create_user_context( + # user: build_stubbed(:user), + # referential: referential + # ) + user_context = OpenStruct.new( + user: build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + context: { referential: referential } + ) + allow(helper).to receive(:current_user).and_return(user_context) + + referentials = [referential] + + allow(referentials).to receive(:model).and_return(Referential) + + allow(helper).to receive(:params).and_return({ + controller: 'workbenches', + action: 'show', + id: referentials[0].workbench.id + }) + + referentials = ModelDecorator.decorate( + referentials, + with: ReferentialDecorator + ) + + expected = <<-HTML +<table class="table has-filter has-search"> + <thead> + <tr> + <th> + <div class="checkbox"><input type="checkbox" name="0" id="0" value="all" /><label for="0"></label></div> + </th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=name">Nom<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=status">Etat<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=organisation">Organisation<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=validity_period">Période de validité englobante<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=lines">Lignes<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=created_at">Créé le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=updated_at">Edité le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=published_at">Intégré le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td> + <div class="checkbox"><input type="checkbox" name="#{referential.id}" id="#{referential.id}" value="#{referential.id}" /><label for="#{referential.id}"></label></div> + </td> + <td title="Voir"><a href="/referentials/#{referential.id}">#{referential.name}</a></td> + <td> + <div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div> + </td> + <td>#{referential.organisation.name}</td> + <td>-</td> + <td>#{referential.lines.count}</td> + <td>#{I18n.localize(referential.created_at, format: :short)}</td> + <td>#{I18n.localize(referential.updated_at, format: :short)}</td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}">Consulter</a></li> + <li><a href="/referentials/#{referential.id}/edit">Editer</a></li> + <li><a href="/referentials/1008/time_tables">Calendriers</a></li> + <li><a href="/referentials/new?from=1008">Dupliquer</a></li> + <li><a rel="nofollow" data-method="put" href="/referentials/#{referential.id}/archive">Conserver</a></li> + <li class="delete-action"><a data-confirm="Etes-vous sûr(e) de vouloir effectuer cette action ?" rel="nofollow" data-method="delete" href="/referentials/#{referential.id}"><span class="fa fa-trash"></span>Supprimer</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML +# TODO: Create a module for the selection box +# <div class="select_toolbox noselect"> +# <ul> +# <li class="st_action"><a data-path="/workbenches/1/referentials" data-confirm="Etes-vous sûr(e) de vouloir effectuer cette action ?" title="Supprimer" rel="nofollow" data-method="delete" href="#"><span class="fa fa-trash"></span></a></li> +# </ul><span class="info-msg"><span>0</span> élément(s) sélectionné(s)</span> +# </div> + + html_str = helper.table_builder_2( + referentials, + [ + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :status, + attribute: Proc.new do |w| + if w.archived? + ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe + else + ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe + end + end + ), + TableBuilderHelper::Column.new( + key: :organisation, + attribute: Proc.new {|w| w.organisation.name} + ), + TableBuilderHelper::Column.new( + key: :validity_period, + attribute: Proc.new do |w| + if w.validity_period.nil? + '-' + else + t( + 'validity_range', + debut: l(w.try(:validity_period).try(:begin), format: :short), + end: l(w.try(:validity_period).try(:end), format: :short) + ) + end + end + ), + TableBuilderHelper::Column.new( + key: :lines, + attribute: Proc.new {|w| w.lines.count} + ), + TableBuilderHelper::Column.new( + key: :created_at, + attribute: Proc.new {|w| l(w.created_at, format: :short)} + ), + TableBuilderHelper::Column.new( + key: :updated_at, + attribute: Proc.new {|w| l(w.updated_at, format: :short)} + ), + TableBuilderHelper::Column.new( + key: :published_at, + attribute: '' + ) + ], + selectable: true, + links: [:show, :edit], + cls: 'table has-filter has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + + it "can set a column as non-sortable" do + company = build_stubbed(:company) + line_referential = build_stubbed( + :line_referential, + companies: [company] + ) + referential = build_stubbed( + :referential, + line_referential: line_referential + ) + + user_context = OpenStruct.new( + user: build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + context: { referential: referential } + ) + allow(helper).to receive(:current_user).and_return(user_context) + allow(TableBuilderHelper::URL).to receive(:current_referential) + .and_return(referential) + + companies = [company] + + allow(companies).to receive(:model).and_return(Chouette::Company) + + allow(helper).to receive(:params).and_return({ + controller: 'referential_companies', + action: 'index', + referential_id: referential.id + }) + + companies = ModelDecorator.decorate( + companies, + with: CompanyDecorator + ) + + expected = <<-HTML +<table class="table has-search"> + <thead> + <tr> + <th>ID Codif</th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=name">Nom<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=phone">Numéro de téléphone<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=email">Email<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=url">Page web associée<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td>#{company.objectid.local_id}</td> + <td title="Voir"><a href="/referentials/#{referential.id}/companies/#{company.id}">#{company.name}</a></td> + <td></td> + <td></td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}/companies/#{company.id}">Consulter</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML + + html_str = helper.table_builder_2( + companies, + [ + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: Proc.new { |n| n.try(:objectid).try(:local_id) }, + sortable: false + ), + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ), + TableBuilderHelper::Column.new( + key: :email, + attribute: 'email' + ), + TableBuilderHelper::Column.new( + key: :url, + attribute: 'url' + ), + ], + links: [:show, :edit, :delete], + cls: 'table has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + + it "can set all columns as non-sortable" do + company = build_stubbed(:company) + line_referential = build_stubbed( + :line_referential, + companies: [company] + ) + referential = build_stubbed( + :referential, + line_referential: line_referential + ) + + user_context = OpenStruct.new( + user: build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + context: { referential: referential } + ) + allow(helper).to receive(:current_user).and_return(user_context) + allow(TableBuilderHelper::URL).to receive(:current_referential) + .and_return(referential) + + companies = [company] + + allow(companies).to receive(:model).and_return(Chouette::Company) + + allow(helper).to receive(:params).and_return({ + controller: 'referential_companies', + action: 'index', + referential_id: referential.id + }) + + companies = ModelDecorator.decorate( + companies, + with: CompanyDecorator + ) + + expected = <<-HTML +<table class="table has-search"> + <thead> + <tr> + <th>ID Codif</th> + <th>Nom</th> + <th>Numéro de téléphone</th> + <th>Email</th> + <th>Page web associée</th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td>#{company.objectid.local_id}</td> + <td title="Voir"><a href="/referentials/#{referential.id}/companies/#{company.id}">#{company.name}</a></td> + <td></td> + <td></td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}/companies/#{company.id}">Consulter</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML + + html_str = helper.table_builder_2( + companies, + [ + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: Proc.new { |n| n.try(:objectid).try(:local_id) } + ), + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ), + TableBuilderHelper::Column.new( + key: :email, + attribute: 'email' + ), + TableBuilderHelper::Column.new( + key: :url, + attribute: 'url' + ), + ], + sortable: false, + links: [:show, :edit, :delete], + cls: 'table has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + end +end + + +# Replace table builder on workspaces#show page +# Make the builder work without a `current_referential` so we can actually test it +# Make a way to define a column as non-sortable. By default, columns will be sortable. Unless sortable==false and no columns should be sortable. +# +# +# TODO: +# - Finish writing workbench test +# - Port some code over to the new table builder +# - Ask Jean-Paul if there's anything he wishes could be changed or improved about the existing table builder +# - Thing that Jean-Paul didn't like was the link generation |
