diff --git a/app/assets/javascripts/prepend.js b/app/assets/javascripts/prepend.js index d8c855e..af56496 100644 --- a/app/assets/javascripts/prepend.js +++ b/app/assets/javascripts/prepend.js @@ -21,16 +21,15 @@ $(function() { if (!$targetRow.data('persisted')) { $targetRow.remove(); } else { - if ($targetRow.data('remove') === 'true') { - $targetRow.data('remove', false); - $targetRow.find('input[type!="hidden"]').attr('disabled', false); - $targetRow.find('input.destroy').val(0); + const currentlyMarkedForRemoval = $targetRow.data('remove') === 'true'; + $targetRow.data('remove', !currentlyMarkedForRemoval); + $targetRow.find('input[type!="hidden"]').attr('disabled', !currentlyMarkedForRemoval); + $targetRow.find('input.destroy').val(Number(!currentlyMarkedForRemoval)); + + if (currentlyMarkedForRemoval) { $targetRow.find('input').removeClass('text-decoration-line-through'); setToDeleteButton($target); } else { - $targetRow.find('input[type!="hidden"]').attr('disabled', true); - $targetRow.data('remove', 'true'); - $targetRow.find('input.destroy').val(1); $targetRow.find('input').addClass('text-decoration-line-through'); setToRestoreButton($target); } @@ -38,27 +37,39 @@ $(function() { }); } - bindDynamicListDeleteHook(); - $('[data-form-prepend]').on('click', function(e) { - const obj = $($(this).attr('data-form-prepend')); - const timestamp = (new Date()).getTime(); - obj.find('input,input[type="hidden"],select,textarea').each(function(_, element) { - $(element).attr('name', function() { - return $(element).attr('name').replace('new_record', timestamp); + function replacePlaceholdersWithTimestamp(element, recordPlaceholder, timestamp) { + Object.entries({ + [recordPlaceholder]: ['name', 'id', 'for', 'data-form-prepend'], + '_timestamp_': ['id', 'class', 'data-dynamic-target-id', 'data-target'], + }).forEach(function([placeholder, attrs]) { + attrs.forEach(function(attr) { + $(element).attr(attr, function() { + const currentAttr = $(element).attr(attr); + if (currentAttr != null) { + return currentAttr.replaceAll(placeholder, timestamp); + } + }); }); }); - // dynamic timestamp selector fill-in - if (obj.attr('id').includes("_timestamp_")) { - obj.attr('id', obj.attr('id').replace("_timestamp_", timestamp)); - } - obj.find("*").each((_, element) => { - const $element = $(element); - const targetId = $element.data('dynamic-target-id'); - if (targetId && targetId.includes("_timestamp_")) { - $element.data('dynamic-target-id', targetId.replace("_timestamp_", timestamp)); - } + } + + function bindDynamicListAddHook() { + // avoid problems from binding a copy of this callback multiple times + $('[data-form-prepend]').off('click'); + $('[data-form-prepend]').on('click', function(e) { + const obj = $($(this).data('form-prepend')); + const childIndexPlaceholder = $(this).data('prepend-child-index'); + const timestamp = (new Date()).getTime(); + obj.find('*').each(function(_, element) { + replacePlaceholdersWithTimestamp(element, childIndexPlaceholder, timestamp); + }); + replacePlaceholdersWithTimestamp(obj, childIndexPlaceholder, timestamp); + $($(this).attr('data-target')).append(obj); + bindDynamicListAddHook(); + bindDynamicListDeleteHook(); }); - $($(this).attr('data-target')).append(obj); - bindDynamicListDeleteHook(); - }); + } + + bindDynamicListAddHook(); + bindDynamicListDeleteHook(); }); diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index f7d0658..3a41629 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -159,6 +159,16 @@ def event_params :state, :zip_code ], + polls_attributes: [ + :id, + :_destroy, + :question, + { + responses_attributes: [ + [:_destroy, :id, :example_response, :choice] + ] + } + ] ) end end diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb deleted file mode 100644 index e1c462b..0000000 --- a/app/controllers/polls_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -class PollsController < ApplicationController - before_action :set_event, except: [:edit, :update, :destroy] - before_action :set_poll, except: [:new, :create] - before_action :authenticate_user! - - def edit - end - - def update - if @poll.update(poll_params) - redirect_to event_path(@poll.event), notice: t('poll.updated') - else - render :edit - end - end - - def new - @poll = Poll.new - end - - def create - @poll = @event.polls.build(poll_params.merge(owner: current_user)) - - if @poll.save - redirect_to event_path(@poll.event), notice: t('poll.created') - else - render :new - end - end - - def destroy - @poll.destroy - - redirect_to event_path(@poll.event), notice: t('poll.destroyed') - end - - private - - def poll_params - params.require(:poll).permit(:question, responses_attributes: [:_destroy, :id, :example_response, :choice]) - end - - def set_event - @event = current_user.managed_events.friendly.find(params[:event_id]) - end - - def set_poll - @poll = current_user.polls.find(params[:id]) - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f876c65..8416bfc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,7 @@ module ApplicationHelper def current_locale_emoji locale_emoji(I18n.locale) end + def locale_emoji(locale) case locale when :en @@ -36,7 +37,7 @@ def link_to_add_fields(name = nil, f = nil, association = nil, options = nil, ht # Render the form fields from a file with the association name provided new_object = f.object.class.reflect_on_association(association).klass.new - options[:child_index] = 'new_record' + options[:child_index] ||= 'new_record' fields = f.fields_for(association, new_object, options) do |builder| render(partial, locals.merge!(f: builder)) end @@ -44,6 +45,7 @@ def link_to_add_fields(name = nil, f = nil, association = nil, options = nil, ht # The rendered fields are sent with the link within the data-form-prepend attr html_options['data-form-prepend'] = raw CGI::escapeHTML( fields ) html_options['data-association-name'] = association + html_options['data-prepend-child-index'] = options[:child_index] html_options['data-target'] = target content_tag(:span, name, html_options, &block) diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 94679c6..d0fd2a0 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -3,9 +3,10 @@ module FormHelper def optional_parent_wrap(parent_form, form_record, options = {}, &block) if parent_form.present? - parent_form.fields_for(:address, options, &block) + record_object = form_record.kind_of?(Array) ? form_record.last : form_record + parent_form.fields_for(record_object, options, &block) else - form_for(form_record, options, &block) + bootstrap_form_for(form_record, options, &block) end end end diff --git a/app/models/event.rb b/app/models/event.rb index ab85a59..8ef25af 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -17,6 +17,7 @@ class Event < ApplicationRecord has_many :polls, dependent: :destroy belongs_to :address + accepts_nested_attributes_for :polls, allow_destroy: true accepts_nested_attributes_for :address, update_only: true validates :start_time, :end_time, presence: true diff --git a/app/models/poll.rb b/app/models/poll.rb index 1885935..8089bd1 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -4,6 +4,8 @@ class Poll < ApplicationRecord belongs_to :event has_many :responses, class_name: "PollResponse", dependent: :destroy + validates :question, presence: true + accepts_nested_attributes_for :responses, allow_destroy: true def responses_and_counts diff --git a/app/views/events/_form.html.haml b/app/views/events/_form.html.haml index 85bc753..2e15e21 100644 --- a/app/views/events/_form.html.haml +++ b/app/views/events/_form.html.haml @@ -2,7 +2,7 @@ .card-header.text-center.text-bg-secondary= t('event.form.preview_label') .hero .hero-image - %small.form-text#crop-instruction Click and drag to adjust the cropping of your image. + %small.form-text#crop-instruction= t('event.form.header_photo_crop_prompt') %img#photo-preview{src: event.photo.attached? ? url_for(event.photo) : image_path('default_event_image.jpg'), data: {initial_y_offset: event.photo_crop_y_offset}} .mx-2 .hero-title @@ -42,5 +42,11 @@ = render "addresses/form", parent_form: form .form-group = form.text_area :description, class: 'form-control', style: "height: 10em;", floating: true, help: t('event.form.description_uses_markdown_html', link: link_to(t('event.form.description_uses_markdown_name'), 'https://www.markdownguide.org/basic-syntax/')) + .form-group.mb-2 + %h3 Polls + #polls + = form.fields_for(:polls, layout: :inline) do |f| + = render partial: "polls/form", locals: {f: f} + = link_to_add_fields t('event.form.add_poll'), form, :polls, {partial: 'polls/form', target: "#polls", locals: { poll: form.object.polls.build }, layout: :inline, child_index: 'added_poll'}, class: 'btn btn-success', id: "add-poll" .actions = form.primary diff --git a/app/views/events/edit.html.haml b/app/views/events/edit.html.haml index b4fdc03..aebb3c0 100644 --- a/app/views/events/edit.html.haml +++ b/app/views/events/edit.html.haml @@ -1,6 +1,4 @@ - title t('event.form.title.edit', title: @event.title) %h1.title= t('event.form.header.edit', title: @event.title) = render 'form', event: @event -= link_to t('form.show'), @event -| = link_to t('back'), events_path diff --git a/app/views/events/show.html.haml b/app/views/events/show.html.haml index 2faed74..bad8f67 100644 --- a/app/views/events/show.html.haml +++ b/app/views/events/show.html.haml @@ -53,8 +53,6 @@ %hr - @event.polls.each do |poll| = render 'polls/poll', poll: poll, respondent: @attendee - - if @event.owned_by?(current_user) - = link_to t('poll.add_prompt'), new_event_poll_path(@event), class: 'btn btn-primary btn-sm my-2' %hr = render @event.root_comments, comments: @event.comments diff --git a/app/views/poll_responses/_edit_example_response.html.haml b/app/views/poll_responses/_edit_example_response.html.haml index 3bc0a19..c079778 100644 --- a/app/views/poll_responses/_edit_example_response.html.haml +++ b/app/views/poll_responses/_edit_example_response.html.haml @@ -3,6 +3,6 @@ = f.hidden_field :id, value: poll_response.id unless poll_response.new_record? = f.hidden_field :_destroy, class: 'destroy', value: 0 = f.hidden_field :example_response, value: 1 - .input-group.m-2 + .input-group = f.text_field :choice, floating: true %button.btn.btn-danger.dynamic-list-delete{data: {dynamic_target_id: row_id}, onclick: "return false;"} X diff --git a/app/views/polls/_form.html.haml b/app/views/polls/_form.html.haml index 44771a2..aa7bbe8 100644 --- a/app/views/polls/_form.html.haml +++ b/app/views/polls/_form.html.haml @@ -1,18 +1,19 @@ -= bootstrap_form_for(form_object) do |f| - - if poll.errors.any? - #error_explanation - %h2= t('poll.form.errors_msg', count: poll.errors.count) - %ul - - poll.errors.full_messages.each do |message| - %li= message - .form-group +- row_id = f.object.persisted? ? "poll_#{f.object.id}" : "poll__timestamp_" +- if f.object.errors.any? + %div{class: "poll_error_explanation_#{f.object.id}"} + %h2= t('poll.form.errors_msg', count: f.object.errors.count) + %ul + - f.object.errors.full_messages.each do |message| + %li= message += f.form_group layout: :inline, class: 'row', "id" => row_id, data: {persisted: (!f.object.new_record?).to_s} do + = f.hidden_field :id, value: f.object.id unless f.object.new_record? + = f.hidden_field :_destroy, class: 'destroy', value: 0 + .input-group = f.text_field :question, floating: true + %button.btn.btn-danger.dynamic-list-delete{data: {dynamic_target_id: row_id}, onclick: "return false;"} X - #example-responses - = f.fields_for(:responses, poll.responses.example, layout: :inline) do |r| - = render partial: 'poll_responses/edit_example_response', locals: {f: r, poll_response: r.object} - = link_to_add_fields t('poll.form.add_option'), f, :responses, {partial: 'poll_responses/edit_example_response', target: "#example-responses", locals: { poll_response: f.object.responses.build }, layout: :inline}, class: 'btn btn-success', id: "add-response" - .actions - = f.submit class: 'btn btn-primary' -- if poll.persisted? - = button_to t('poll.form.destroy'), poll_path(poll), method: :delete, class: 'btn btn-danger', data: {confirm: t('poll.form.confirm_destroy')} + .my-2.px-4 + %div.mb-2{class: "example-responses-#{row_id}"} + = f.fields_for(:responses, scope: :example, layout: :inline) do |r| + = render partial: 'poll_responses/edit_example_response', locals: {f: r, poll_response: r.object} + = link_to_add_fields t('poll.form.add_option'), f, :responses, {partial: 'poll_responses/edit_example_response', target: ".example-responses-#{row_id}", locals: { poll_response: f.object.responses.build }, layout: :inline, child_index: 'added_response'}, class: 'btn btn-success', id: "add-response" diff --git a/app/views/polls/_poll.html.haml b/app/views/polls/_poll.html.haml index a4deb9e..19f0a23 100644 --- a/app/views/polls/_poll.html.haml +++ b/app/views/polls/_poll.html.haml @@ -1,8 +1,6 @@ .my-2 %h4 = poll.question - - if poll.owned_by?(current_user) - %small= link_to t('form.edit'), edit_poll_path(poll), class: 'text-muted' - if respondent.nil? %em.text-muted= t('poll.rejection.unauthenticated_html', sign_in_link: link_to(t('poll.rejection.unauthenticated_sign_in_msg'), new_user_session_path)) %ul.list-group diff --git a/app/views/polls/edit.html.haml b/app/views/polls/edit.html.haml deleted file mode 100644 index 09e0788..0000000 --- a/app/views/polls/edit.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- title t('poll.form.title.edit', name: @poll.question) -%h1.title= t('poll.form.header.edit', name: @poll.question) -= render 'form', poll: @poll, form_object: [@poll] -= link_to t('back'), event_path(@poll.event), class: 'button is-white' diff --git a/app/views/polls/new.html.haml b/app/views/polls/new.html.haml deleted file mode 100644 index e629063..0000000 --- a/app/views/polls/new.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- title t('poll.form.title.new') -%h1.title= t('poll.form.header.new') -= render 'form', poll: @poll, form_object: [@event, @poll] -= link_to t('back'), event_path(@event), class: 'button is-white' diff --git a/config/environments/development.rb b/config/environments/development.rb index dcff1d8..ae83ce1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,3 +1,5 @@ +require "core_extensions/console_methods" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -53,4 +55,8 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + module Rails::ConsoleMethods + include CoreExtensions::ConsoleMethods + end end diff --git a/config/environments/test.rb b/config/environments/test.rb index ad904de..db5fa17 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,5 @@ +require "core_extensions/console_methods" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -41,4 +43,8 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + + module Rails::ConsoleMethods + include CoreExtensions::ConsoleMethods + end end diff --git a/config/locales/models/event/en.yml b/config/locales/models/event/en.yml index 6ab42f2..0281996 100644 --- a/config/locales/models/event/en.yml +++ b/config/locales/models/event/en.yml @@ -41,7 +41,7 @@ en: Events that require testing for COVID-19 will have a banner informing guests of the testing requirement. Be sure to explain how to send you evidence of a negative test in your event description. header_photo_help: Photos are scaled and cropped to full-screen width (1900px by 500px). - header_photo_crop_prompt: Click and drag the header image above to change how your header image will be cropped. + header_photo_crop_prompt: Click and drag the header image to change how your header image will be cropped. description_uses_markdown_html: You can format the event description with %{link} to make text bold, add links, or organize things into bullet points. description_uses_markdown_name: Markdown labels: diff --git a/config/locales/models/event/es.yml b/config/locales/models/event/es.yml index dd5f3be..e68022f 100644 --- a/config/locales/models/event/es.yml +++ b/config/locales/models/event/es.yml @@ -43,7 +43,7 @@ es: Eventos que requieren pruebas de COVID-19 tendrá un aviso que informará a los invitados sobre el requisito de la prueba. Asegúrese de explicar cómo enviarle evidencia de una prueba negativa en la descripción del evento. header_photo_help: Fotos se escalan y recortan al ancho de pantalla completo (1900px por 500px). - header_photo_crop_prompt: Haga clic y arrastre la imagen de arriba para cambiar cómo se recortará su foto. + header_photo_crop_prompt: Haga clic y arrastre la imagen de encabezado para cambiar cómo se recortará su foto. description_uses_markdown_html: Se puede formatear la descripción del evento con %{link} para poner el texto en negrita, agregar enlaces, o organizar cosas en viñetas. description_uses_markdown_name: Markdown labels: diff --git a/config/locales/views/home/es.yml b/config/locales/views/home/es.yml index c137f2d..5ba528b 100644 --- a/config/locales/views/home/es.yml +++ b/config/locales/views/home/es.yml @@ -13,9 +13,9 @@ es: host_prompt: logged_in: full_text: "%{bolded_section}, presiona al botón de abajo para organizar un evento nuevo, o presiona a \"Organizar un Evento\" debajo de \"Hospedador\" en la barra de navegación." - bolded_section: Si está aquí para hospedar a su propio evento + bolded_section: Si está aquí para organizar su propio evento unauthenticated: full_text: "%{bolded_section}, primero debe iniciar sesión. Presiona al botón de abajo para iniciar sesión, o presiona a 'Iniciar sesión' en la barra de navegación." - bolded_section: Si está aquí para hospedar su propio evento + bolded_section: Si está aquí para organizar su propio evento new_event_button: Organizar un evento log_in_button: Iniciar sesión diff --git a/lib/core_extensions/console_methods.rb b/lib/core_extensions/console_methods.rb new file mode 100644 index 0000000..b37009e --- /dev/null +++ b/lib/core_extensions/console_methods.rb @@ -0,0 +1,9 @@ +# helpful functions for debugging and exploration + +module CoreExtensions + module ConsoleMethods + def unique_methods(thing) + (thing.public_methods - Object.public_methods).sort + end + end +end