diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 24a433285..2040a02ce 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -545,7 +545,7 @@ footer { // 04. Tags // - - - - - - - - - - - - - - - - - - - - - - - - - -.tags a , .tag-cloud a, .categories a, .geozone a { +.tags a , .tag-cloud a, .categories a, .geozone a, .sidebar-links a { background: #ececec; border-radius: rem-calc(6); color: $text; @@ -1730,9 +1730,12 @@ table { border: 0; td { - padding-left: $line-height*1.5; position: relative; - word-break: break-all; + + &:first-child { + padding-left: $line-height*1.5; + width: 80%; + } &:before { color: $brand; @@ -1743,19 +1746,31 @@ table { } } - &.activity-comments td:before { + &.activity-comments td:first-child:before { content: "e"; top: 18px; } - &.activity-debates td:before { + &.activity-debates td:first-child:before { content: "i"; top: 14px; } - &.activity-proposals td:before { - content: "h"; - top: 18px; + &.activity-proposals { + + td:first-child:before { + content: "h"; + top: 18px; + } + + .retired { + text-decoration: line-through; + } + } + + &.activity-investment-projects td:first-child:before { + content: "\53"; + top: 10px; } } } diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 8ede20c2b..7ceaf7b99 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -343,6 +343,10 @@ word-wrap: break-word; } + .callout.proposal-retired { + font-size: $base-font-size; + } + .social-share-full .social-share-button { display:inline; } diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index add59a7cb..f778b2ca0 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -23,7 +23,14 @@ class ProposalsController < ApplicationController end def index_customization - @featured_proposals = Proposal.all.sort_by_confidence_score.limit(3) if (!@advanced_search_terms && @search_terms.blank? && @tag_filter.blank?) + if params[:retired].present? + @resources = @resources.retired + @resources = @resources.where(retired_reason: params[:retired]) if Proposal::RETIRE_OPTIONS.include?(params[:retired]) + else + @resources = @resources.not_retired + end + + @featured_proposals = Proposal.all.sort_by_confidence_score.limit(3) if (!@advanced_search_terms && @search_terms.blank? && @tag_filter.blank? && params[:retired].blank?) if @featured_proposals.present? set_featured_proposal_votes(@featured_proposals) @resources = @resources.where('proposals.id NOT IN (?)', @featured_proposals.map(&:id)) @@ -35,6 +42,17 @@ class ProposalsController < ApplicationController set_proposal_votes(@proposal) end + def retire + if valid_retired_params? && @proposal.update(retired_params.merge(retired_at: Time.now)) + redirect_to proposal_path(@proposal), notice: t('proposals.notice.retired') + else + render action: :retire_form + end + end + + def retire_form + end + def vote_featured @proposal.register_vote(current_user, 'yes') set_featured_proposal_votes(@proposal) @@ -51,6 +69,16 @@ class ProposalsController < ApplicationController params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :captcha, :captcha_key, :geozone_id) end + def retired_params + params.require(:proposal).permit(:retired_reason, :retired_explanation) + end + + def valid_retired_params? + @proposal.errors.add(:retired_reason, I18n.t('errors.messages.blank')) if params[:proposal][:retired_reason].blank? + @proposal.errors.add(:retired_explanation, I18n.t('errors.messages.blank')) if params[:proposal][:retired_explanation].blank? + @proposal.errors.empty? + end + def resource_model Proposal end diff --git a/app/helpers/proposals_helper.rb b/app/helpers/proposals_helper.rb index f01abc053..9eaa88549 100644 --- a/app/helpers/proposals_helper.rb +++ b/app/helpers/proposals_helper.rb @@ -28,4 +28,8 @@ module ProposalsHelper end end + def retire_proposals_options + Proposal::RETIRE_OPTIONS.collect { |option| [ t("proposals.retire_options.#{option}"), option ] } + end + end \ No newline at end of file diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 5bb43725b..6dd36d5b0 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -16,6 +16,7 @@ module Abilities can :update, Proposal do |proposal| proposal.editable_by?(user) end + can [:retire_form, :retire], Proposal, author_id: user.id can :read, SpendingProposal diff --git a/app/models/proposal.rb b/app/models/proposal.rb index f437fa134..e6071e052 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -12,6 +12,8 @@ class Proposal < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + RETIRE_OPTIONS = %w(duplicated started unfeasible done other) + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable @@ -26,6 +28,7 @@ class Proposal < ActiveRecord::Base validates :description, length: { maximum: Proposal.description_max_length } validates :question, length: { in: 10..Proposal.question_max_length } validates :responsible_name, length: { in: 6..Proposal.responsible_name_max_length } + validates :retired_reason, inclusion: {in: RETIRE_OPTIONS, allow_nil: true} validates :terms_of_service, acceptance: { allow_nil: false }, on: :create @@ -42,6 +45,8 @@ class Proposal < ActiveRecord::Base scope :sort_by_relevance, -> { all } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} + scope :retired, -> { where.not(retired_at: nil) } + scope :not_retired, -> { where(retired_at: nil) } def to_param "#{id}-#{title}".parameterize @@ -105,6 +110,10 @@ class Proposal < ActiveRecord::Base user && user.level_two_or_three_verified? end + def retired? + retired_at.present? + end + def register_vote(user, vote_value) if votable_by?(user) vote_by(voter: user, vote: vote_value) diff --git a/app/views/proposals/_retired.html.erb b/app/views/proposals/_retired.html.erb new file mode 100644 index 000000000..4ab1aba98 --- /dev/null +++ b/app/views/proposals/_retired.html.erb @@ -0,0 +1,13 @@ + + + +<% if params[:retired].blank? %> +

<%= link_to t("proposals.index.retired_proposals_link"), proposals_path(retired: 'all'), class: "small" %>

+<% else %> + +<% end %> diff --git a/app/views/proposals/index.html.erb b/app/views/proposals/index.html.erb index 970e1cb3e..67ee98f67 100644 --- a/app/views/proposals/index.html.erb +++ b/app/views/proposals/index.html.erb @@ -22,6 +22,8 @@ <%= page_entries_info @proposals %> <%= t("proposals.index.filter_topic", count: @proposals.size, topic: @tag_filter) %> + <% elsif params[:retired].present? %> +

<%= t("proposals.index.retired_proposals") %> <% end %> @@ -38,7 +40,7 @@ <% end %> - <%= render "shared/advanced_search", search_path: proposals_path(page: 1)%> + <%= render("shared/advanced_search", search_path: proposals_path(page: 1)) unless params[:retired].present? %> <%= render 'shared/order_links', i18n_namespace: "proposals.index" %> @@ -53,10 +55,13 @@
diff --git a/app/views/proposals/retire_form.html.erb b/app/views/proposals/retire_form.html.erb new file mode 100644 index 000000000..d56277cb3 --- /dev/null +++ b/app/views/proposals/retire_form.html.erb @@ -0,0 +1,38 @@ +
+ +
+ +

<%= t("proposals.retire_form.title") %>

+ +

<%= link_to @proposal.title, @proposal %>

+ +
+ <%= t("proposals.retire_form.warning") %> +
+ + <%= form_for(@proposal, url: retire_proposal_path(@proposal)) do |f| %> + <%= render 'shared/errors', resource: @proposal %> +
+
+ <%= f.label :retired_reason, t("proposals.retire_form.retired_reason_label") %> + <%= f.select :retired_reason, retire_proposals_options, {include_blank: t("proposals.retire_form.retired_reason_blank"), label: false} %> +
+
+ +
+
+ <%= f.label :retired_explanation, t("proposals.retire_form.retired_explanation_label") %> + <%= f.text_area :retired_explanation, rows: 4, maxlength: 500, label: false, + placeholder: t('proposals.retire_form.retired_explanation_placeholder') %> +
+
+ +
+
+ <%= f.submit(class: "button expanded", value: t("proposals.retire_form.submit_button")) %> +
+
+ <% end %> + +
+
\ No newline at end of file diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index 0a5b35c0c..c319c1409 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -22,7 +22,14 @@ <% end %>

<%= @proposal.title %>

- <% if @proposal.conflictive? %> + <% if @proposal.retired? %> +
+ + <%= t("proposals.show.retired_warning") %>
+ <%= link_to t("proposals.show.retired_warning_link_to_explanation"), "#retired_explanation" %> +
+
+ <% elsif @proposal.conflictive? %>
<%= t("proposals.show.flag") %>
@@ -74,6 +81,13 @@

<%= @proposal.question %>

+ <% if @proposal.retired? %> +
+

<%= t('proposals.show.retired') %>: <%= t("proposals.retire_options.#{@proposal.retired_reason}") unless @proposal.retired_reason == 'other' %>

+ <%= simple_format text_with_links(@proposal.retired_explanation), {}, sanitize: false %> +
+ <% end %> + <%= render 'shared/tags', taggable: @proposal %> <%= render 'shared/geozone', geozonable: @proposal %> diff --git a/app/views/users/_proposals.html.erb b/app/views/users/_proposals.html.erb index a8ec5061e..9ec1368bd 100644 --- a/app/views/users/_proposals.html.erb +++ b/app/views/users/_proposals.html.erb @@ -2,10 +2,19 @@ <% @proposals.each do |proposal| %> - <%= link_to proposal.title, proposal %> + <%= link_to proposal.title, proposal, proposal.retired? ? {class: 'retired'} : {} %>
<%= proposal.summary %> + + <% if proposal.retired? %> + <%= t('users.show.retired') %> + <% else %> + <%= link_to t('users.show.retire'), + retire_form_proposal_path(proposal), + class: 'delete' %> + <% end %> + <% end %> diff --git a/app/views/users/_spending_proposals.html.erb b/app/views/users/_spending_proposals.html.erb index 04bfa7867..40fd8d70d 100644 --- a/app/views/users/_spending_proposals.html.erb +++ b/app/views/users/_spending_proposals.html.erb @@ -1,15 +1,16 @@ - +
<% @spending_proposals.each do |spending_proposal| %> + diff --git a/config/locales/en.yml b/config/locales/en.yml index 729effd4c..de37b5975 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -254,6 +254,20 @@ en: form: submit_button: Save changes show_link: View proposal + retire_form: + title: Retire proposal + warning: "If you retire the proposal it would still accept supports, but will be removed from the main list and a message will be visible to all users stating that the author considers the proposal should not be supported anymore" + retired_reason_label: Reason to retire the proposal + retired_reason_blank: Choose an option + retired_explanation_label: Explanation + retired_explanation_placeholder: Explain shortly why you think this proposal should not receive more supports + submit_button: Retire proposal + retire_options: + duplicated: Duplicated + started: Already underway + unfeasible: Unfeasible + done: Done + other: Other form: geozone: Scope of operation proposal_external_url: Link to additional documentation @@ -282,6 +296,15 @@ en: hot_score: most active most_commented: most commented relevance: relevance + retired_proposals: Retired proposals + retired_proposals_link: "Proposals retired by the author" + retired_links: + all: All + duplicated: Duplicated + started: Underway + unfeasible: Unfeasible + done: Done + other: Other search_form: button: Search placeholder: Search proposals... @@ -305,6 +328,8 @@ en: recommendation_two: Any proposal or comment suggesting illegal action will be deleted, as well as those intending to sabotage the debate spaces. Anything else is allowed. recommendations_title: Recommendations for creating a proposal start_new: Create new proposal + notice: + retired: Proposal retired proposal: already_supported: You have already supported this proposal. Share it! comments: @@ -333,6 +358,9 @@ en: edit_proposal_link: Edit flag: This proposal has been flagged as inappropriate by several users. login_to_comment: You must %{signin} or %{signup} to leave a comment. + retired_warning: "The author considers this proposal should not receive more supports." + retired_warning_link_to_explanation: Read the explanation before voting for it. + retired: Proposal retired by the author share: Share update: form: @@ -490,6 +518,8 @@ en: other: "%{count} Spending proposals" no_activity: User has no public activity private_activity: This user decided to keep the activity list private + retire: Retire + retired: Retired votes: agree: I agree anonymous: Too many anonymous votes to admit vote %{verify_account}. diff --git a/config/locales/es.yml b/config/locales/es.yml index 437bd8c97..82b7d59d1 100755 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -254,6 +254,20 @@ es: form: submit_button: Guardar cambios show_link: Ver propuesta + retire_form: + title: Retirar propuesta + warning: "Si sigues adelante tu propuesta podrá seguir recibiendo apoyos, pero dejará de ser listada en la lista principal, y aparecerá un mensaje para todos los usuarios avisándoles de que el autor considera que esta propuesta no debe seguir recogiendo apoyos." + retired_reason_label: Razón por la que se retira la propuesta + retired_reason_blank: Selecciona una opción + retired_explanation_label: Explicación + retired_explanation_placeholder: Explica brevemente por que consideras que esta propuesta no debe recoger más apoyos + submit_button: Retirar propuesta + retire_options: + duplicated: Duplicada + started: Ejecutándose + unfeasible: Inviable + done: Hecha + other: Otra form: geozone: "Ámbito de actuación" proposal_external_url: Enlace a documentación adicional @@ -282,6 +296,15 @@ es: hot_score: Más activas hoy most_commented: Más comentadas relevance: Más relevantes + retired_proposals: Propuestas retiradas + retired_proposals_link: "Propuestas retiradas por sus autores" + retired_links: + all: Todas + duplicated: Duplicadas + started: Empezadas + unfeasible: Inviables + done: Hechas + other: Otras search_form: button: Buscar placeholder: Buscar propuestas... @@ -305,6 +328,8 @@ es: recommendation_two: Cualquier propuesta o comentario que implique una acción ilegal será eliminada, también las que tengan la intención de sabotear los espacios de propuesta, todo lo demás está permitido. recommendations_title: Recomendaciones para crear una propuesta start_new: Crear una propuesta + notice: + retired: Propuesta retirada proposal: already_supported: "¡Ya has apoyado esta propuesta, compártela!" comments: @@ -333,6 +358,9 @@ es: edit_proposal_link: Editar propuesta flag: Esta propuesta ha sido marcada como inapropiada por varios usuarios. login_to_comment: Necesitas %{signin} o %{signup} para comentar. + retired_warning: "El autor de esta propuesta considera que ya no debe seguir recogiendo apoyos." + retired_warning_link_to_explanation: Revisa su explicación antes de apoyarla. + retired: Propuesta retirada por el autor share: Compartir update: form: @@ -490,6 +518,8 @@ es: other: "%{count} Propuestas de inversión" no_activity: Usuario sin actividad pública private_activity: Este usuario ha decidido mantener en privado su lista de actividades + retire: Retirar + retired: Retirada votes: agree: Estoy de acuerdo anonymous: Demasiados votos anónimos, para poder votar %{verify_account}. diff --git a/config/routes.rb b/config/routes.rb index 43b8b5219..6c9b629f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,8 @@ Rails.application.routes.draw do post :vote_featured put :flag put :unflag + get :retire_form + patch :retire end collection do get :map diff --git a/db/migrate/20160420105023_add_retired_to_proposals.rb b/db/migrate/20160420105023_add_retired_to_proposals.rb new file mode 100644 index 000000000..f818a9169 --- /dev/null +++ b/db/migrate/20160420105023_add_retired_to_proposals.rb @@ -0,0 +1,5 @@ +class AddRetiredToProposals < ActiveRecord::Migration + def change + add_column :proposals, :retired_at, :datetime, default: nil + end +end diff --git a/db/migrate/20160422094733_add_retired_texts_to_proposals.rb b/db/migrate/20160422094733_add_retired_texts_to_proposals.rb new file mode 100644 index 000000000..3c044477d --- /dev/null +++ b/db/migrate/20160422094733_add_retired_texts_to_proposals.rb @@ -0,0 +1,6 @@ +class AddRetiredTextsToProposals < ActiveRecord::Migration + def change + add_column :proposals, :retired_reason, :string, default: nil + add_column :proposals, :retired_explanation, :text, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 49f0011f2..7e83ab7cc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160418172919) do +ActiveRecord::Schema.define(version: 20160422094733) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -239,27 +239,30 @@ ActiveRecord::Schema.define(version: 20160418172919) do add_index "organizations", ["user_id"], name: "index_organizations_on_user_id", using: :btree create_table "proposals", force: :cascade do |t| - t.string "title", limit: 80 + t.string "title", limit: 80 t.text "description" t.string "question" t.string "external_url" t.integer "author_id" t.datetime "hidden_at" - t.integer "flags_count", default: 0 + t.integer "flags_count", default: 0 t.datetime "ignored_flag_at" - t.integer "cached_votes_up", default: 0 - t.integer "comments_count", default: 0 + t.integer "cached_votes_up", default: 0 + t.integer "comments_count", default: 0 t.datetime "confirmed_hide_at" - t.integer "hot_score", limit: 8, default: 0 - t.integer "confidence_score", default: 0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "responsible_name", limit: 60 + t.integer "hot_score", limit: 8, default: 0 + t.integer "confidence_score", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "responsible_name", limit: 60 t.text "summary" t.string "video_url" - t.integer "physical_votes", default: 0 + t.integer "physical_votes", default: 0 t.tsvector "tsv" t.integer "geozone_id" + t.datetime "retired_at" + t.string "retired_reason" + t.text "retired_explanation" end add_index "proposals", ["author_id", "hidden_at"], name: "index_proposals_on_author_id_and_hidden_at", using: :btree diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb index bde574935..4a3ea58a1 100644 --- a/spec/features/proposals_spec.rb +++ b/spec/features/proposals_spec.rb @@ -88,27 +88,27 @@ feature 'Proposals' do end context "Embedded video" do - scenario "Show YouTube video" do + scenario "Show YouTube video" do proposal = create(:proposal, video_url: "http://www.youtube.com/watch?v=a7UFm6ErMPU") visit proposal_path(proposal) expect(page).to have_selector("div[id='js-embedded-video']") expect(page.html).to include 'https://www.youtube.com/embed/a7UFm6ErMPU' end - - scenario "Show Vimeo video" do + + scenario "Show Vimeo video" do proposal = create(:proposal, video_url: "https://vimeo.com/7232823" ) visit proposal_path(proposal) expect(page).to have_selector("div[id='js-embedded-video']") expect(page.html).to include 'https://player.vimeo.com/video/7232823' end - - scenario "Dont show video" do + + scenario "Dont show video" do proposal = create(:proposal, video_url: nil) visit proposal_path(proposal) expect(page).to_not have_selector("div[id='js-embedded-video']") - end - end + end + end scenario 'Social Media Cards' do proposal = create(:proposal) @@ -375,7 +375,7 @@ feature 'Proposals' do end end - context "Geozones" do + context 'Geozones' do scenario "Default whole city" do author = create(:user) @@ -430,6 +430,100 @@ feature 'Proposals' do end + context 'Retired proposals' do + scenario 'Retire' do + proposal = create(:proposal) + login_as(proposal.author) + + visit user_path(proposal.author) + within("#proposal_#{proposal.id}") do + click_link 'Retire' + end + expect(current_path).to eq(retire_form_proposal_path(proposal)) + + select 'Duplicated', from: 'proposal_retired_reason' + fill_in 'proposal_retired_explanation', with: 'There are three other better proposals with the same subject' + click_button "Retire proposal" + + expect(page).to have_content "Proposal retired" + + visit proposal_path(proposal) + + expect(page).to have_content proposal.title + expect(page).to have_content 'Proposal retired by the author' + expect(page).to have_content 'Duplicated' + expect(page).to have_content 'There are three other better proposals with the same subject' + end + + scenario 'Fields are mandatory' do + proposal = create(:proposal) + login_as(proposal.author) + + visit retire_form_proposal_path(proposal) + + click_button 'Retire proposal' + + expect(page).to_not have_content 'Proposal retired' + expect(page).to have_content "can't be blank", count: 2 + end + + scenario 'Index do not list retired proposals by default' do + create_featured_proposals + not_retired = create(:proposal) + retired = create(:proposal, retired_at: Time.now) + + visit proposals_path + + expect(page).to have_selector('#proposals .proposal', count: 1) + within('#proposals') do + expect(page).to have_content not_retired.title + expect(page).to_not have_content retired.title + end + end + + scenario 'Index has a link to retired proposals list' do + create_featured_proposals + not_retired = create(:proposal) + retired = create(:proposal, retired_at: Time.now) + + visit proposals_path + + expect(page).to_not have_content retired.title + click_link 'Proposals retired by the author' + + expect(page).to have_content retired.title + expect(page).to_not have_content not_retired.title + end + + scenario 'Retired proposals index interface elements' do + visit proposals_path(retired: 'all') + + expect(page).to_not have_content 'Advanced search' + expect(page).to_not have_content 'Categories' + expect(page).to_not have_content 'Districts' + end + + scenario 'Retired proposals index has links to filter by retired_reason' do + unfeasible = create(:proposal, retired_at: Time.now, retired_reason: 'unfeasible') + duplicated = create(:proposal, retired_at: Time.now, retired_reason: 'duplicated') + + visit proposals_path(retired: 'all') + + expect(page).to have_content unfeasible.title + expect(page).to have_content duplicated.title + expect(page).to have_link 'Duplicated' + expect(page).to have_link 'Underway' + expect(page).to have_link 'Unfeasible' + expect(page).to have_link 'Done' + expect(page).to have_link 'Other' + + click_link 'Unfeasible' + + expect(page).to have_content unfeasible.title + expect(page).to_not have_content duplicated.title + end + end + scenario 'Update should not be posible if logged user is not the author' do proposal = create(:proposal) expect(proposal).to be_editable diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index eae4d4fbd..4958204ec 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -741,4 +741,30 @@ describe Proposal do end end + describe "retired" do + before(:all) do + @proposal1 = create(:proposal) + @proposal2 = create(:proposal, retired_at: Time.now) + end + + it "retired? is true" do + expect(@proposal1.retired?).to eq false + expect(@proposal2.retired?).to eq true + end + + it "scope retired" do + retired = Proposal.retired + + expect(retired.size).to eq(1) + expect(retired.first).to eq(@proposal2) + end + + it "scope not_retired" do + not_retired = Proposal.not_retired + + expect(not_retired.size).to eq(1) + expect(not_retired.first).to eq(@proposal1) + end + end + end
<%= link_to spending_proposal.title, spending_proposal %> - + <% if can?(:destroy, spending_proposal) %> <%= link_to t("users.show.delete_spending_proposal"), spending_proposal, method: :delete, data: { confirm: t("users.show.confirm_deletion_spending_proposal") }, - class: 'button small warning' %> + class: 'delete' %> <% end %>