From 199bc00c087759249e16f662f130bef348c2898b Mon Sep 17 00:00:00 2001 From: James Mason Date: Mon, 9 Apr 2018 11:53:31 -0700 Subject: [PATCH 1/3] Add (admin-only) tickets via 'visibile' attribute [migration] (cherry picked from commit ce0db3d7a736e7bcde480721986dbfb4a82aa083) --- app/controllers/admin/tickets_controller.rb | 7 ++++- .../conference_registrations_controller.rb | 2 +- app/controllers/conferences_controller.rb | 2 +- app/controllers/tickets_controller.rb | 7 ++++- app/models/ability.rb | 5 +++- app/models/ticket.rb | 2 ++ app/models/ticket_purchase.rb | 2 +- app/views/admin/tickets/_form.html.haml | 4 +++ app/views/admin/tickets/index.html.haml | 3 ++ .../conference_registrations/show.html.haml | 2 +- app/views/tickets/index.html.haml | 4 +-- .../20180409170433_add_visible_to_tickets.rb | 11 +++++++ db/schema.rb | 1 + spec/factories/tickets.rb | 1 + spec/features/tickets_spec.rb | 29 +++++++++++++++++++ 15 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 db/migrate/20180409170433_add_visible_to_tickets.rb diff --git a/app/controllers/admin/tickets_controller.rb b/app/controllers/admin/tickets_controller.rb index e0e5b186..ef309716 100644 --- a/app/controllers/admin/tickets_controller.rb +++ b/app/controllers/admin/tickets_controller.rb @@ -52,7 +52,12 @@ module Admin private def ticket_params - params.require(:ticket).permit(:conference, :title, :url, :description, :conference_id, :price_cents, :price_currency, :price, :registration_ticket) + params.require(:ticket).permit( + :conference, :conference_id, + :title, :url, :description, + :price_cents, :price_currency, :price, + :registration_ticket, :visible + ) end end end diff --git a/app/controllers/conference_registrations_controller.rb b/app/controllers/conference_registrations_controller.rb index 70cfc736..4dd60447 100644 --- a/app/controllers/conference_registrations_controller.rb +++ b/app/controllers/conference_registrations_controller.rb @@ -57,7 +57,7 @@ class ConferenceRegistrationsController < ApplicationController sign_in(@registration.user) end - if @conference.tickets.any? && !current_user.supports?(@conference) + if @conference.tickets.visible.any? && !current_user.supports?(@conference) redirect_to conference_tickets_path(@conference.short_title), notice: 'You are now registered and will be receiving E-Mail notifications.' else diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index 43809551..ba156daf 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -54,7 +54,7 @@ class ConferencesController < ApplicationController end end if splashpage.include_registrations || splashpage.include_tickets - @tickets = @conference.tickets.order(:price_cents) + @tickets = @conference.tickets.visible.order('price_cents') end if splashpage.include_lodgings @lodgings = @conference.lodgings.order(:name) diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index c62afc06..7766d006 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -3,7 +3,8 @@ class TicketsController < ApplicationController before_action :authenticate_user! load_resource :conference, find_by: :short_title - load_resource :ticket, through: :conference + before_action :load_tickets + authorize_resource :ticket, through: :conference authorize_resource :conference_registrations, class: Registration before_action :check_load_resource, only: :index @@ -14,4 +15,8 @@ class TicketsController < ApplicationController redirect_to root_path, notice: "There are no tickets available for #{@conference.title}!" end end + + def load_tickets + @tickets = @conference.tickets.visible + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 913c97ff..4ab1bd16 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -85,7 +85,10 @@ class Ability end end - can :index, Ticket + can :index, Organization + can :index, Ticket do |ticket| + ticket.visible + end can :manage, TicketPurchase, user_id: user.id can [:new, :create], Payment, user_id: user.id can [:index, :show], PhysicalTicket, user: user diff --git a/app/models/ticket.rb b/app/models/ticket.rb index abc014cd..41f8c8fe 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -20,6 +20,8 @@ class Ticket < ApplicationRecord validates :price_cents, numericality: { greater_than_or_equal_to: 0 } + scope :visible, -> { where(visible: true) } + def bought?(user) buyers.include?(user) end diff --git a/app/models/ticket_purchase.rb b/app/models/ticket_purchase.rb index 6d373d3f..ecdcebfa 100644 --- a/app/models/ticket_purchase.rb +++ b/app/models/ticket_purchase.rb @@ -32,7 +32,7 @@ class TicketPurchase < ApplicationRecord errors.push('You cannot buy more than one registration tickets.') else ActiveRecord::Base.transaction do - conference.tickets.each do |ticket| + conference.tickets.visible.each do |ticket| quantity = purchases[ticket.id.to_s].to_i # if the user bought the ticket and is still unpaid, just update the quantity purchase = if ticket.bought?(user) && ticket.unpaid?(user) diff --git a/app/views/admin/tickets/_form.html.haml b/app/views/admin/tickets/_form.html.haml index 54f82c50..b4ab27bc 100644 --- a/app/views/admin/tickets/_form.html.haml +++ b/app/views/admin/tickets/_form.html.haml @@ -1,3 +1,4 @@ +<<<<<<< HEAD = form_for(@ticket, url: (@ticket.new_record? ? admin_conference_tickets_path : admin_conference_ticket_path(@conference.short_title, @ticket))) do |f| .form-group = f.label :title @@ -21,5 +22,8 @@ %label = f.check_box :registration_ticket A registration ticket is with which user register for the conference. + %label + = f.check_box :visible + Only visible tickets are available to registrants. Non-visible tickets can only be managed by Admins. %p.text-right = f.submit nil, class: 'btn btn-primary' diff --git a/app/views/admin/tickets/index.html.haml b/app/views/admin/tickets/index.html.haml index 369adbc1..fc632ac8 100644 --- a/app/views/admin/tickets/index.html.haml +++ b/app/views/admin/tickets/index.html.haml @@ -16,6 +16,7 @@ %th Sold %th Turnover %th Registration Ticket + %th Visible? %th Actions %tbody - @conference.tickets.each do |ticket| @@ -31,6 +32,8 @@ = humanized_money_with_symbol ticket.tickets_turnover_total(ticket.id) %td = ticket.registration_ticket? ? 'Yes' : 'No' + %td + = ticket.visible? ? 'Yes' : 'No' %td .btn-group = link_to 'Edit', edit_admin_conference_ticket_path(@conference.short_title, ticket.id), diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 3955331e..fac80e3c 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -74,7 +74,7 @@ = link_to event.title, conference_program_proposal_path(@conference.short_title, event.id) = '(' + registered_text(event) + ')' - - if @conference.tickets.any? + - if @conference.tickets.visible.any? .row .col-md-12 %h4 diff --git a/app/views/tickets/index.html.haml b/app/views/tickets/index.html.haml index 104cdce0..5f0a7611 100644 --- a/app/views/tickets/index.html.haml +++ b/app/views/tickets/index.html.haml @@ -19,8 +19,8 @@ %th Price %th Total %tbody - - @conference.tickets.each do |ticket| - = render 'ticket', f: f, ticket: ticket + - @conference.tickets.visible.each do |ticket| + = render partial: 'ticket', f: f, locals: {ticket: ticket} %tr %td - if @conference.tickets.for_registration.any? diff --git a/db/migrate/20180409170433_add_visible_to_tickets.rb b/db/migrate/20180409170433_add_visible_to_tickets.rb new file mode 100644 index 00000000..199f7a48 --- /dev/null +++ b/db/migrate/20180409170433_add_visible_to_tickets.rb @@ -0,0 +1,11 @@ +class AddVisibleToTickets < ActiveRecord::Migration[5.0] + def up + add_column :tickets, :visible, :boolean, default: true + Ticket.reset_column_information + Ticket.update_all(visible: true) # rubocop:disable Rails/SkipsModelValidations + end + + def down + remove_column :tickets, :visible + end +end diff --git a/db/schema.rb b/db/schema.rb index 298e77a3..287a6b87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -522,6 +522,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_11_21_114727) do t.boolean "registration_ticket", default: false t.datetime "created_at" t.datetime "updated_at" + t.boolean "visible", default: true end create_table "tracks", force: :cascade do |t| diff --git a/spec/factories/tickets.rb b/spec/factories/tickets.rb index e71b898f..42d877d0 100644 --- a/spec/factories/tickets.rb +++ b/spec/factories/tickets.rb @@ -5,6 +5,7 @@ FactoryBot.define do title { "#{Faker::Hipster.word} Ticket" } price_cents { 1000 } price_currency { 'USD' } + visible { true } factory :registration_ticket do registration_ticket { true } end diff --git a/spec/features/tickets_spec.rb b/spec/features/tickets_spec.rb index ed0f6d22..6ba790a2 100644 --- a/spec/features/tickets_spec.rb +++ b/spec/features/tickets_spec.rb @@ -29,6 +29,35 @@ feature Ticket do expect(Ticket.count).to eq(2) end + scenario 'add a invalid ticket', feature: true, js: true do + visit admin_conference_tickets_path(conference.short_title) + click_link 'Add Ticket' + + fill_in 'ticket_title', with: '' + fill_in 'ticket_price', with: '-1' + + click_button 'Create Ticket' + page.find('#flash') + expect(flash).to eq("Creating Ticket failed: Title can't be blank. Price cents must be greater than or equal to 0.") + expect(Ticket.count).to eq(1) + end + + scenario 'add a hidden ticket', feature: true do + visit admin_conference_tickets_path(conference.short_title) + click_link 'Add Ticket' + + fill_in 'ticket_title', with: 'Hidden Ticket' + fill_in 'ticket_description', with: 'The hidden ticket' + fill_in 'ticket_price', with: '100' + uncheck 'ticket_visible' + + click_button 'Create Ticket' + page.find('#flash') + expect(flash).to eq('Ticket successfully created.') + expect(Ticket.count).to eq(2) + expect(Ticket.visible.count).to eq(1) + end + context 'Ticket already created' do let!(:ticket) { create(:ticket, title: 'Business Ticket', price: 100, conference_id: conference.id) } From 4b40f409e5f52bac6c335e526fd6c39fd1f226f9 Mon Sep 17 00:00:00 2001 From: James Mason Date: Mon, 9 Apr 2018 14:00:45 -0700 Subject: [PATCH 2/3] Admins can give tickets. (cherry picked from commit aeeeb8e331673011842a35946a1bdba2611f901e) --- .rubocop_todo.yml | 1 + app/controllers/admin/tickets_controller.rb | 28 +- app/views/admin/tickets/show.html.haml | 31 ++- config/routes.rb | 6 +- .../admin/tickets_controller_spec.rb | 247 ++++++++++++++++++ 5 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 spec/controllers/admin/tickets_controller_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 400f5949..19983ff3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -660,6 +660,7 @@ RSpec/AnyInstance: Exclude: - 'spec/controllers/admin/rooms_controller_spec.rb' - 'spec/controllers/admin/sponsorship_levels_controller_spec.rb' + - 'spec/controllers/admin/tickets_controller_spec.rb' - 'spec/controllers/admin/tracks_controller_spec.rb' - 'spec/controllers/admin/users_controller_spec.rb' - 'spec/controllers/conference_registration_controller_spec.rb' diff --git a/app/controllers/admin/tickets_controller.rb b/app/controllers/admin/tickets_controller.rb index ef309716..eb1d620e 100644 --- a/app/controllers/admin/tickets_controller.rb +++ b/app/controllers/admin/tickets_controller.rb @@ -38,13 +38,30 @@ module Admin end end + def give + ticket_purchase = @ticket.ticket_purchases.new(gift_ticket_params) + recipient = ticket_purchase.user + if ticket_purchase.save + redirect_to( + admin_conference_ticket_path(@conference.short_title, @ticket), + notice: "#{recipient.name} was given a #{@ticket.title} ticket." + ) + else + redirect_back( + fallback_location: admin_conference_ticket_path(@conference.short_title, @ticket), + error: "Unable to give #{recipient.name} a #{@ticket.title} ticket: " + + ticket_purchase.errors.full_messages.to_sentence + ) + end + end + def destroy if @ticket.destroy redirect_to admin_conference_tickets_path(conference_id: @conference.short_title), - notice: 'Ticket successfully destroyed.' + notice: 'Ticket successfully deleted.' else redirect_to admin_conference_tickets_path(conference_id: @conference.short_title), - error: 'Ticket was successfully destroyed.' \ + error: 'Deleting ticket failed! ' \ "#{@ticket.errors.full_messages.join('. ')}." end end @@ -59,5 +76,12 @@ module Admin :registration_ticket, :visible ) end + + def gift_ticket_params + response = params.require(:ticket_purchase).permit( + :user_id + ) + response.merge(paid: true, amount_paid: 0, conference: @conference) + end end end diff --git a/app/views/admin/tickets/show.html.haml b/app/views/admin/tickets/show.html.haml index 6fa74639..6ce20a20 100644 --- a/app/views/admin/tickets/show.html.haml +++ b/app/views/admin/tickets/show.html.haml @@ -7,7 +7,13 @@ Ticket %small = humanized_money_with_symbol @ticket.price - = link_to 'Edit Ticket', edit_admin_conference_ticket_path, class: 'btn btn-primary pull-right' + = link_to 'Edit Ticket', edit_admin_conference_ticket_path, class: 'btn btn-primary pull-right' + - if can? :give, Ticket + .pull-right + = link_to 'Give a Ticket', '#', + data: { toggle: 'modal', target: "#modal-give-ticket-#{@ticket.id}" }, + class: 'button btn btn-default btn-info' + %p.text-muted People who bought this ticket .row @@ -36,3 +42,26 @@ = buyer.affiliation %td = @ticket.tickets_paid(buyer) + +- content_for :modals do + .modal.fade{ id: "modal-give-ticket-#{@ticket.id}" } + .modal-dialog + .modal-content + = semantic_form_for(@ticket.ticket_purchases.new, + url: give_admin_conference_ticket_path(@conference, @ticket)) do |f| + .modal-header + %button.close{ data: { dismiss: 'modal' } } + %i.fa.fa-close + %h3.modal-title + Give a + = @ticket.title + Ticket + .modal-body + = user_selector_input(:user, f, '', false) + .modal-footer + = f.action :submit, as: :button, button_html: { class: 'btn btn-primary' } + +:javascript + $(document).ready(function() { + $('#ticket_purchase_user_id').selectize({}) + }); diff --git a/config/routes.rb b/config/routes.rb index 730d43cf..49246f30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -112,7 +112,11 @@ Osem::Application.routes.draw do end resources :resources - resources :tickets + resources :tickets do + member do + post :give + end + end resources :sponsors, except: [:show] resources :lodgings, except: [:show] resources :emails, only: [:show, :update, :index] diff --git a/spec/controllers/admin/tickets_controller_spec.rb b/spec/controllers/admin/tickets_controller_spec.rb new file mode 100644 index 00000000..7e668b68 --- /dev/null +++ b/spec/controllers/admin/tickets_controller_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::TicketsController do + let(:admin) { create(:admin) } + let(:conference) { create(:conference) } + let!(:ticket) { create(:ticket, conference: conference) } + let(:new_title) { Faker::Hipster.sentence } + + context 'admin is signed in' do + before { sign_in admin } + + describe 'GET #index' do + before { get :index, conference_id: conference } + + it 'assigns conference and tickets variables via cancancan' do + expect(assigns(:conference)).to eq conference + expect(assigns(:tickets)).to eq conference.tickets + end + + it 'renders index template' do + expect(response).to render_template('index') + end + end + + describe 'GET #edit' do + before { get :edit, conference_id: conference, id: ticket } + + it 'assigns ticket variable via cancancan' do + expect(assigns(:ticket)).to eq ticket + end + + it 'renders edit template' do + expect(response).to render_template('edit') + end + end + + describe 'GET #new' do + before { get :new, conference_id: conference } + + it 'assigns ticket variable' do + expect(assigns(:ticket)).to be_instance_of(Ticket) + end + + it 'renders new template' do + expect(response).to render_template('new') + end + end + + describe 'POST #create' do + context 'saves successfuly' do + before(:each, run: true) do + post :create, conference_id: conference, ticket: attributes_for(:ticket) + end + + let!(:ticket_count) { conference.tickets.count } + + it 'redirects to index path', run: true do + expect(response).to redirect_to( + admin_conference_tickets_path(conference_id: conference) + ) + end + + it 'shows success message in flash notice', run: true do + expect(flash[:notice]).to match('Ticket successfully created.') + end + + it 'creates new ticket' do + expect do + post :create, ticket: attributes_for(:ticket), + conference_id: conference + end.to change{ conference.tickets.count }.from(ticket_count).to(ticket_count + 1) + end + end + + context 'save fails' do + before do + allow_any_instance_of(Ticket).to receive(:save).and_return(false) + post :create, conference_id: conference, ticket: attributes_for(:ticket) + end + + let!(:ticket_count) { conference.tickets.count } + + it 'renders new template' do + expect(response).to render_template('new') + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Creating Ticket failed: #{ticket.errors.full_messages.join('. ')}.") + end + + it 'does not create new ticket' do + expect(conference.tickets.count).to eq ticket_count + end + end + end + + describe 'PATCH #update' do + context 'updates successfully' do + before do + patch :update, conference_id: conference, id: ticket, + ticket: attributes_for(:ticket, title: new_title) + end + + it 'redirects to index path' do + expect(response).to redirect_to( + admin_conference_tickets_path(conference_id: conference) + ) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match('Ticket successfully updated.') + end + + it 'updates the ticket' do + ticket.reload + expect(ticket.title).to eq(new_title) + end + end + + context 'update fails' do + before do + allow_any_instance_of(Ticket).to receive(:save).and_return(false) + patch :update, conference_id: conference, id: ticket, + ticket: attributes_for(:ticket, title: new_title) + end + + it 'renders edit template' do + expect(response).to render_template('edit') + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Ticket update failed: #{ticket.errors.full_messages.join('. ')}.") + end + + it 'does not update ticket' do + ticket.reload + expect(ticket.title).not_to eq(new_title) + end + end + end + + describe 'DELETE #destroy' do + context 'deletes successfully' do + before(:each, run: true) do + delete :destroy, conference_id: conference, id: ticket + end + + let!(:ticket_count) { conference.tickets.count } + + it 'redirects to index path', run: true do + expect(response).to redirect_to( + admin_conference_tickets_path(conference_id: conference) + ) + end + + it 'shows success message in flash notice', run: true do + expect(flash[:notice]).to match('Ticket successfully deleted.') + end + + it 'deletes the ticket' do + expect do + delete :destroy, conference_id: conference, id: ticket + end.to change{ conference.tickets.count }.from(ticket_count).to(ticket_count - 1) + end + end + + context 'delete fails' do + let!(:ticket_count) { conference.tickets.count } + + before do + allow_any_instance_of(Ticket).to receive(:destroy).and_return(false) + delete :destroy, conference_id: conference, id: ticket + end + + it 'redirects to index path' do + expect(response).to redirect_to( + admin_conference_tickets_path(conference_id: conference) + ) + end + + it 'shows error in flash message' do + expect(flash[:error]).to match("Deleting ticket failed! #{ticket.errors.full_messages.join('. ')}.") + end + + it 'does not delete ticket' do + expect(conference.tickets.count).to eq(ticket_count) + end + end + end + + describe 'POST #give' do + context 'grants a ticket purchase to a user' do + let!(:purchase_count) { admin.ticket_purchases.count } + + before do + post :give, conference_id: conference, id: ticket, + ticket_purchase: { user_id: admin.id } + end + + it 'redirects to ticket' do + expect(response).to redirect_to( + admin_conference_ticket_path(conference_id: conference, id: ticket) + ) + end + + it 'shows success message in flash notice' do + expect(flash[:notice]).to match( + "#{admin.name} was given a #{ticket.title} ticket." + ) + end + + it 'creates a ticket purchase' do + expect(admin.ticket_purchases.count).to eq(purchase_count + 1) + expect(admin.ticket_purchases.last.ticket).to eq(ticket) + end + end + + context 'giving fails' do + before do + allow_any_instance_of(TicketPurchase).to receive(:save).and_return(false) + post :give, conference_id: conference, id: ticket, + ticket_purchase: { user_id: admin.id } + end + + let(:purchase_count) { admin.ticket_purchases.count } + + it 'redirects to ticket' do + expect(response).to redirect_to( + admin_conference_ticket_path(conference_id: conference, id: ticket) + ) + end + + it 'shows error in flash message' do + expect(flash[:error]).to match( + "Unable to give #{admin.name} a #{ticket.title} ticket: " + ) + end + + it 'does not create a ticket purchase' do + expect(admin.ticket_purchases.count).to eq(purchase_count) + end + end + end + end +end From 003d272e744747c1f2b3248593125570ec318149 Mon Sep 17 00:00:00 2001 From: James Mason Date: Fri, 28 Dec 2018 11:12:24 -0800 Subject: [PATCH 3/3] Run some features headless #BetterPlace --- spec/features/tickets_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/tickets_spec.rb b/spec/features/tickets_spec.rb index 6ba790a2..78d61a31 100644 --- a/spec/features/tickets_spec.rb +++ b/spec/features/tickets_spec.rb @@ -15,7 +15,7 @@ feature Ticket do sign_out end - scenario 'add a valid ticket', feature: true, js: true do + scenario 'add a valid ticket', feature: true do visit admin_conference_tickets_path(conference.short_title) click_link 'Add Ticket' @@ -29,7 +29,7 @@ feature Ticket do expect(Ticket.count).to eq(2) end - scenario 'add a invalid ticket', feature: true, js: true do + scenario 'add a invalid ticket', feature: true do visit admin_conference_tickets_path(conference.short_title) click_link 'Add Ticket' @@ -61,7 +61,7 @@ feature Ticket do context 'Ticket already created' do let!(:ticket) { create(:ticket, title: 'Business Ticket', price: 100, conference_id: conference.id) } - scenario 'edit valid ticket', feature: true, js: true do + scenario 'edit valid ticket', feature: true do visit admin_conference_tickets_path(conference.short_title) click_link('Edit', href: edit_admin_conference_ticket_path(conference.short_title, ticket.id))