diff --git a/app/controllers/api/v1/events_controller.rb b/app/controllers/api/v1/events_controller.rb index 0e9a20c..0a61e78 100644 --- a/app/controllers/api/v1/events_controller.rb +++ b/app/controllers/api/v1/events_controller.rb @@ -8,13 +8,26 @@ def index code: event.code, name: event.name, description: event.description.body ? event.description.body.to_html : "", - address: event.address, + address: { name: event.event_place.name, + street: event.event_place.street, + number: event.event_place.number, + neighborhood: event.event_place.neighborhood, + city: event.event_place.city, + zip_code: event.event_place.zip_code, + state: event.event_place.state }, logo_url: event.logo.attached? ? url_for(event.logo) : nil, banner_url: event.banner.attached? ? url_for(event.banner) : nil, participants_limit: event.participants_limit, event_owner: event.user.name, start_date: event.start_date, - end_date: event.end_date + end_date: event.end_date, + recommendations: event.event_place.event_place_recommendations.map do |recommendation| + { + name: recommendation.name, + full_address: recommendation.full_address, + phone: recommendation.phone + } + end } end } @@ -31,7 +44,13 @@ def show code: event.code, name: event.name, description: event.description.body ? event.description.body.to_html : "", - address: event.address, + address: { name: event.event_place.name, + street: event.event_place.street, + number: event.event_place.number, + neighborhood: event.event_place.neighborhood, + city: event.event_place.city, + zip_code: event.event_place.zip_code, + state: event.event_place.state }, logo_url: event.logo.attached? ? url_for(event.logo) : nil, banner_url: event.banner.attached? ? url_for(event.banner) : nil, participants_limit: event.participants_limit, diff --git a/app/controllers/api/v1/speakers_controller.rb b/app/controllers/api/v1/speakers_controller.rb index cf14a80..03e97c6 100644 --- a/app/controllers/api/v1/speakers_controller.rb +++ b/app/controllers/api/v1/speakers_controller.rb @@ -22,7 +22,13 @@ def events name: event.name, event_type: event.event_type, description: event.description.body ? event.description.body.to_html : "", - address: event.address, + address: { name: event.event_place.name, + street: event.event_place.street, + number: event.event_place.number, + neighborhood: event.event_place.neighborhood, + city: event.event_place.city, + zip_code: event.event_place.zip_code, + state: event.event_place.state }, logo_url: event.logo.attached? ? url_for(event.logo) : nil, banner_url: event.banner.attached? ? url_for(event.banner) : nil, participants_limit: event.participants_limit, @@ -44,7 +50,13 @@ def event name: event.name, event_type: event.event_type, description: event.description.body ? event.description.body.to_html : "", - address: event.address, + address: { name: event.event_place.name, + street: event.event_place.street, + number: event.event_place.number, + neighborhood: event.event_place.neighborhood, + city: event.event_place.city, + zip_code: event.event_place.zip_code, + state: event.event_place.state }, logo_url: event.logo.attached? ? url_for(event.logo) : nil, banner_url: event.banner.attached? ? url_for(event.banner) : nil, participants_limit: event.participants_limit, diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 0b61402..6b36f4c 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -62,7 +62,7 @@ def history private def event_params - params.require(:event).permit(:name, :address, :event_type, :participants_limit, :url, :logo, :banner, :description, :start_date, :end_date, category_ids: []) + params.require(:event).permit(:name, :event_type, :participants_limit, :url, :logo, :banner, :description, :start_date, :end_date, :event_place_id, category_ids: []) end def authorize_event_access diff --git a/app/models/event.rb b/app/models/event.rb index 7237b9e..b0a13af 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -4,6 +4,7 @@ class Event < ApplicationRecord default_scope -> { kept } belongs_to :user + belongs_to :event_place has_one_attached :logo has_one_attached :banner @@ -19,13 +20,13 @@ class Event < ApplicationRecord validates :code, uniqueness: true validates :name, :participants_limit, :url, :status, :start_date, :end_date, presence: true - validates :address, presence: true, if: -> { inperson? || hybrid? } validates :logo, content_type: { in: [ "image/png", "image/jpeg", "image/jpg" ], message: "deve ser uma imagem do tipo PNG, JPG ou JPEG" } validates :banner, content_type: { in: [ "image/png", "image/jpeg", "image/jpg" ], message: "deve ser uma imagem do tipo PNG, JPG ou JPEG" } validates :start_date, :end_date, comparison: { greater_than: Time.now, message: "não pode ser depois da data atual" } validates :start_date, comparison: { less_than: :end_date, message: "não pode ser depois da data de fim", if: -> { end_date.present? } } validate :participants_limit_for_unverified_user validate :should_have_at_least_one_category + # validate :should_have_an_event_place after_create :set_schedules @@ -50,6 +51,10 @@ def should_have_at_least_one_category errors.add(:categories, "deve ter ao menos uma categoria") if categories.empty? end + # def should_have_an_event_place + # errors.add(:event_place, "deve ter um local de evento") if self.inperson? || self.hybrid? && event_place.nil? + # end + def generate_code loop do self.code = SecureRandom.alphanumeric(8).upcase diff --git a/app/models/event_place.rb b/app/models/event_place.rb index 5534354..f88fb87 100644 --- a/app/models/event_place.rb +++ b/app/models/event_place.rb @@ -1,5 +1,6 @@ class EventPlace < ApplicationRecord belongs_to :user + has_many :events has_one_attached :photo diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index 929fd8a..e93765b 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -20,16 +20,6 @@ <% end %> -
- <%= f.label :address, class: 'input_label' %> - <%= f.text_field :address, class: "input"%> - <% if event.errors[:address].any? %> - <% event.errors.full_messages_for(:address).each do |e| %> - <%= e %> - <% end %> - <% end %> -
-
<%= f.label :participants_limit, class: 'input_label' %> <%= f.number_field :participants_limit,class: "input"%> @@ -95,6 +85,16 @@ <% end %>
+
+ <%= f.label :event_place_id, "Local do Evento", class: 'input_label' %> + <%= f.collection_select :event_place_id, current_user.event_places.all, :id, :name, { prompt: 'Escolha um local de evento' }, { class: 'input' } %> + <% if event.errors[:event_places].any? %> + <% event.errors.full_messages_for(:event_places).each do |e| %> + <%= e %> + <% end %> + <% end %> +
+
<%= f.label :categories, class: 'input_label' %> <%= f.collection_checkboxes(:category_ids, Category.all, :id, :name) do |b| %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index d27461b..cd35c13 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -65,10 +65,16 @@ <%= @event.url %>

- <% if @event.address %> + <% if @event.event_place %>

Endereço: - <%= @event.address %> + <%= @event.event_place.name %> + <%= @event.event_place.street %> + <%= @event.event_place.number %> + <%= @event.event_place.neighborhood %> + <%= @event.event_place.city %> + <%= @event.event_place.zip_code %> + <%= @event.event_place.state %>

<% end %> diff --git a/db/migrate/20250207190453_remove_address_from_event.rb b/db/migrate/20250207190453_remove_address_from_event.rb new file mode 100644 index 0000000..2d97bab --- /dev/null +++ b/db/migrate/20250207190453_remove_address_from_event.rb @@ -0,0 +1,5 @@ +class RemoveAddressFromEvent < ActiveRecord::Migration[8.0] + def change + remove_column :events, :address, :string + end +end diff --git a/db/migrate/20250207190710_add_event_place_ref_to_event_.rb b/db/migrate/20250207190710_add_event_place_ref_to_event_.rb new file mode 100644 index 0000000..f9565a2 --- /dev/null +++ b/db/migrate/20250207190710_add_event_place_ref_to_event_.rb @@ -0,0 +1,5 @@ +class AddEventPlaceRefToEvent < ActiveRecord::Migration[8.0] + def change + add_reference :events, :event_place, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9e86795..f922bca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_02_05_212037) do +ActiveRecord::Schema[8.0].define(version: 2025_02_07_190710) do create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" @@ -113,7 +113,6 @@ t.string "name" t.integer "user_id", null: false t.integer "event_type" - t.string "address" t.integer "participants_limit" t.string "url" t.integer "status" @@ -123,8 +122,10 @@ t.string "code", null: false t.datetime "start_date" t.datetime "end_date" + t.integer "event_place_id" t.index ["code"], name: "index_events_on_code" t.index ["discarded_at"], name: "index_events_on_discarded_at" + t.index ["event_place_id"], name: "index_events_on_event_place_id" t.index ["user_id"], name: "index_events_on_user_id" end @@ -244,6 +245,7 @@ add_foreign_key "event_categories", "events" add_foreign_key "event_place_recommendations", "event_places" add_foreign_key "event_places", "users" + add_foreign_key "events", "event_places" add_foreign_key "events", "users" add_foreign_key "schedule_items", "schedules" add_foreign_key "schedules", "events" diff --git a/db/seeds.rb b/db/seeds.rb index 39af9ad..3571c84 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -42,7 +42,6 @@ ruby_event = FactoryBot.create(:event, name: 'Conferencia Ruby', event_type: :online, - address: 'Sem endereço', participants_limit: 25, url: 'confruby.com.br', status: :draft, @@ -57,10 +56,10 @@ ruby_event.banner.attach(io: File.open(Rails.root.join('spec/support/images/banner_ruby.png')), filename: 'banner_ruby.png') puts 'Criando evento CONFERENCIA JAVASCRIPT...' + javascript_event = FactoryBot.create(:event, name: 'Conferencia JS', event_type: :inperson, - address: 'Rua dos Computadores, 125', participants_limit: 30, url: 'confjs.com.br', status: :published, @@ -68,17 +67,19 @@ categories: [ javascript_category ], start_date: 2.weeks.from_now, end_date: 3.weeks.from_now, - description: 'Um evento maneiro de Java escrito' + description: 'Um evento maneiro de Java escrito', ) + javascript_event.logo.attach(io: File.open(Rails.root.join('spec/support/images/javascript.png')), filename: 'javascript.png') sleep(5) javascript_event.banner.attach(io: File.open(Rails.root.join('spec/support/images/banner_javascript.png')), filename: 'banner_javascript.png') + puts 'Criando evento TROPICAL ON RAILS...' + tropical_event = FactoryBot.create(:event, name: 'Tropical on Rails 2025', event_type: :hybrid, - address: 'Auditório Hotel Pullman - Vila Olímpia, São Paulo - SP', participants_limit: 30, url: 'www.evento.com', status: :published, @@ -88,15 +89,18 @@ end_date: 1.weeks.from_now, description: "O Tropical on Rails 2025 é a Conferência Latam de Rails e tem como objetivo fortalecer a comunidade de Rails da América Latina para que ela continue sendo uma parte integral do presente e do futuro do Ruby on Rails. O que antes era bom como Tropical.rb agora ficou melhor ainda sendo Tropical On Rails, nossa estrutura também cresceu e nessa edição vamos ter 700 com palestrantes incríveis estarão no nosso palco: Xavier Noria, Chris Oliver, Rosa Gutiérrez, Irina Nazarova, Rafael França, Vinicius Stock e muitos outros." ) + + tropical_event.logo.attach(io: File.open(Rails.root.join('spec/support/images/logo.jpg')), filename: 'logo.jpg') sleep(5) tropical_event.banner.attach(io: File.open(Rails.root.join('spec/support/images/banner.png')), filename: 'banner.png') + puts 'Criando evento RUBY SUMMIT BRASIL 2025...' + ruby_summit_event = FactoryBot.create(:event, name: 'Ruby Summit Brasil 2025', event_type: :inperson, - address: 'Teatro Renaissance - São Paulo, SP', participants_limit: 30, url: 'www.rubysummitbr.com', status: :published, @@ -106,14 +110,16 @@ end_date: (1.month.from_now + 1.day), description: "O Ruby Summit Brasil 2025 reúne a comunidade Ruby brasileira em um evento repleto de palestras, painéis e workshops com os melhores especialistas do mercado. Com keynotes internacionais e espaço para networking, é a oportunidade ideal para aprender e compartilhar conhecimento sobre Ruby e suas tecnologias relacionadas." ) + + ruby_summit_event.logo.attach(io: File.open(Rails.root.join('spec/support/images/ruby-summit-brasil.png')), filename: 'ruby-summit-brasil.png') sleep(5) puts 'Criando evento FULL STACK CONF 2025...' + full_stack_conf_event = FactoryBot.create(:event, name: 'Full Stack Conf 2025', event_type: :online, - address: nil, participants_limit: 30, url: 'www.fullstackconf.com', status: :published, diff --git a/spec/factories/event_places.rb b/spec/factories/event_places.rb index 275c3cd..5e36426 100644 --- a/spec/factories/event_places.rb +++ b/spec/factories/event_places.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :event_place do - name { "Arena de Grêmio" } + sequence(:name) { |n| "Arena de Grêmio #{n}" } street { "Av. Padre Leopoldo Brentano" } - number { "110" } + sequence(:number) { |n| "#{110 + n}" } neighborhood { "Farrapos" } city { "Porto Alegre" } zip_code { "90250590" } diff --git a/spec/factories/events.rb b/spec/factories/events.rb index c22be8a..2ba06ef 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -1,8 +1,6 @@ FactoryBot.define do factory :event do name { "Lollapalooza" } - event_type { :inperson } - address { "Av dos Bancos" } participants_limit { 30 } url { "http://Lollapalooza.com" } association :user @@ -10,6 +8,7 @@ end_date { (Time.now + 5.weeks) } banner { File.open(Rails.root.join('spec/support/images/no_banner.png'), filename: 'no_banner.png') } logo { File.open(Rails.root.join('spec/support/images/no_logo.png'), filename: 'no_logo.png') } + event_place { create :event_place } categories { [ create(:category) ] } end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 31d0251..3b10de0 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory(:user) do - email { 'alice@email.com' } + sequence(:email) { |n| "user#{n}@example.com" } password { 'password123' } password_confirmation { 'password123' } name { 'Alice' } diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index a66aceb..b64a4eb 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -11,10 +11,10 @@ end it 'falso quando endereço não está preenchido' do - event = FactoryBot.build(:event, address: '') + event = FactoryBot.build(:event, event_place: nil) event.valid? - expect(event.errors[:address]).to include 'não pode ficar em branco' + expect(event.errors[:event_place]).to include 'é obrigatório(a)' expect(event).not_to be_valid end @@ -109,10 +109,10 @@ end it 'endereço deve ser obrigatório somente quando o evento for presencial ou hibrido' do - event = FactoryBot.build(:event, event_type: :online, address: '') + event = FactoryBot.build(:event, event_type: :online) event.valid? - expect(event.errors).not_to include(:address) + expect(event.errors).not_to include(:event_place) expect(event).to be_valid end diff --git a/spec/requests/api/v1/event_api_request_spec.rb b/spec/requests/api/v1/event_api_request_spec.rb index 5b8cc25..d0689c2 100644 --- a/spec/requests/api/v1/event_api_request_spec.rb +++ b/spec/requests/api/v1/event_api_request_spec.rb @@ -9,7 +9,7 @@ event = build( :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', - address: 'Rua das Laranjeiras, 123', description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, + description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, start_date: (Time.now + 1.day).change(hour: 8, min: 0, sec: 0), end_date: (Time.now + 3.day).change(hour: 18, min: 0, sec: 0)) @@ -20,14 +20,13 @@ draft_event = create( :event, name: 'Formação de Padeiros', user: user, status: 'draft', - address: 'Rua dos ipês, 343', description: 'Aprenda a fazer Pão como um profissional', categories: [ category ]) + description: 'Aprenda a fazer Pão como um profissional', categories: [ category ]) get '/api/v1/events' expect(response).to have_http_status :success expect(response.content_type).to include('application/json') expect(response.parsed_body['events'][0]['name']).to include(event.name) - expect(response.parsed_body['events'][0]['address']).to include(event.address) expect(response.parsed_body['events'][0]['description']).to include(event.description.body.to_html) expect(response.parsed_body['events'][0]['code']).to eq event.code expect(response.parsed_body['events'][0]['logo_url']).to eq url_for(event.logo) @@ -37,7 +36,6 @@ expect(response.parsed_body['events'][0]['start_date']).to eq event.start_date.iso8601(3) expect(response.parsed_body['events'][0]['end_date']).to eq event.end_date.iso8601(3) expect(response.parsed_body['events']).not_to include(draft_event.name) - expect(response.parsed_body['events']).not_to include(draft_event.address) expect(response.parsed_body['events']).not_to include(draft_event.description) expect(response.parsed_body['events']).not_to include(draft_event.id) end @@ -48,8 +46,8 @@ category = Category.create!(name: 'Palestra') event = build( - :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', - address: 'Rua das Laranjeiras, 123', description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, + :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', event_type: 'online', + description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, start_date: (Time.now + 1.day).change(hour: 8, min: 0, sec: 0), end_date: (Time.now + 3.day).change(hour: 18, min: 0, sec: 0)) event.logo.attach(io: File.open('spec/support/images/logo.png'), filename: 'logo.png', content_type: 'img/png') @@ -58,15 +56,14 @@ event.save create( - :event, name: 'Formação de Padeiros', user: user, status: 'published', - address: 'Rua dos ipês, 343', description: 'Aprenda a fazer Pão como um profissional', categories: [ category ]) + :event, name: 'Formação de Padeiros', user: user, status: 'published', event_type: 'online', + description: 'Aprenda a fazer Pão como um profissional', categories: [ category ]) get '/api/v1/events', params: { query: event.name } expect(response).to have_http_status :success expect(response.content_type).to include('application/json') expect(response.parsed_body['events'][0]['name']).to include(event.name) - expect(response.parsed_body['events'][0]['address']).to include(event.address) expect(response.parsed_body['events'][0]['description']).to include(event.description.body.to_html) expect(response.parsed_body['events'][0]['code']).to eq event.code expect(response.parsed_body['events'][0]['logo_url']).to eq url_for(event.logo) @@ -77,14 +74,49 @@ expect(response.parsed_body['events'][0]['end_date']).to eq event.end_date.iso8601(3) expect(response.parsed_body['events'].count).to eq 1 end + + it 'e há um local recomendado' do + user = create(:user) + event_place = create(:event_place, user: user) + + event = build( + :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', event_type: 'hybrid', + description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, event_place: event_place, + start_date: (Time.now + 1.day).change(hour: 8, min: 0, sec: 0), end_date: (Time.now + 3.day).change(hour: 18, min: 0, sec: 0)) + + + event.logo.attach(io: File.open('spec/support/images/logo.png'), filename: 'logo.png', content_type: 'img/png') + event.banner.attach(io: File.open('spec/support/images/banner.jpg'), filename: 'banner.png', content_type: 'img/jpg') + + event.save + + event_place_recommendation = create(:event_place_recommendation, event_place: event_place) + + get '/api/v1/events' + + expect(response).to have_http_status :success + expect(response.content_type).to include('application/json') + expect(response.parsed_body['events'][0]['name']).to include(event.name) + expect(response.parsed_body['events'][0]['address']['name']).to include(event.event_place.name) + expect(response.parsed_body['events'][0]['address']['street']).to include(event.event_place.street) + expect(response.parsed_body['events'][0]['address']['number']).to include(event.event_place.number) + expect(response.parsed_body['events'][0]['address']['neighborhood']).to include(event.event_place.neighborhood) + expect(response.parsed_body['events'][0]['address']['city']).to include(event.event_place.city) + expect(response.parsed_body['events'][0]['address']['zip_code']).to include(event.event_place.zip_code) + expect(response.parsed_body['events'][0]['address']['state']).to include(event.event_place.state) + expect(response.parsed_body['events'][0]['recommendations'][0]['name']).to include(event_place_recommendation.name) + expect(response.parsed_body['events'][0]['recommendations'][0]['full_address']).to include(event_place_recommendation.full_address) + expect(response.parsed_body['events'][0]['recommendations'][0]['phone']).to include(event_place_recommendation.phone) + end end context 'Usuário ve detalhes' do it 'com sucesso' do user = create(:user) + event_place = create(:event_place, user: user) event = build( - :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', - address: 'Rua das Laranjeiras, 123', description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, + :event, name: 'Formação de Churrasqueiros', user: user, status: 'published', event_place: event_place, + description: 'Aprenda a fazer churrasco como um profissional', participants_limit: 30, start_date: (Time.now + 3.day).change(hour: 8, min: 0, sec: 0), end_date: (Time.now + 4.day).change(hour: 18, min: 0, sec: 0)) event.logo.attach(io: File.open('spec/support/images/logo.png'), filename: 'logo.png', content_type: 'img/png') event.banner.attach(io: File.open('spec/support/images/banner.jpg'), filename: 'banner.png', content_type: 'img/jpg') @@ -101,7 +133,7 @@ expect(response).to have_http_status :success expect(response.content_type).to include('application/json') expect(response.parsed_body['name']).to include(event.name) - expect(response.parsed_body['address']).to include(event.address) + expect(response.parsed_body['address']['name']).to include(event.event_place.name) expect(response.parsed_body['description']).to include(event.description.body.to_html) expect(response.parsed_body['code']).to eq event.code expect(response.parsed_body['logo_url']).to eq url_for(event.logo) diff --git a/spec/system/events/user_creates_event_spec.rb b/spec/system/events/user_creates_event_spec.rb index 37b7008..2906f0d 100644 --- a/spec/system/events/user_creates_event_spec.rb +++ b/spec/system/events/user_creates_event_spec.rb @@ -24,12 +24,14 @@ Category.create!(name: 'Festa') Category.create!(name: 'Palestra') + other_place = FactoryBot.create(:event_place) + event_place = FactoryBot.create(:event_place, name: 'Casa do João', street: 'Rua do João', number: '123', neighborhood: 'Centro', city: 'São Paulo', zip_code: '12345-678', state: 'SP') + login_as user visit new_event_path fill_in 'Nome', with: 'Lollapaluza' - fill_in 'Endereço', with: 'Rua X' select 'Presencial', from: 'Tipo de evento' fill_in 'Limite de participantes', with: 30 fill_in 'URL do evento', with: 'www.Lollapaluza.com' @@ -38,6 +40,7 @@ attach_file('Logo', Rails.root.join('spec/support/images/logo.png')) attach_file('Banner', Rails.root.join('spec/support/images/banner.jpg')) find('trix-editor').click.set('test') + select 'Casa do João', from: 'Local do evento' check 'Festa' check 'Palestra' click_on 'Criar' diff --git a/spec/system/events/user_edit_event_spec.rb b/spec/system/events/user_edit_event_spec.rb index 8c0ac08..ca4623d 100644 --- a/spec/system/events/user_edit_event_spec.rb +++ b/spec/system/events/user_edit_event_spec.rb @@ -5,7 +5,7 @@ it 'com sucesso' do user = create(:user) category = create(:category, name: 'Tecnologia') - create(:event, user: user, name: 'Introdução ao RoR', categories: [ category ]) + create(:event, user: user, name: 'Introdução ao RoR', categories: [ category ], event_type: :online) login_as user visit root_path