diff --git a/app/controllers/advertisements_controller.rb b/app/controllers/advertisements_controller.rb
new file mode 100644
index 0000000..850e230
--- /dev/null
+++ b/app/controllers/advertisements_controller.rb
@@ -0,0 +1,42 @@
+class AdvertisementsController < ApplicationController
+ before_action :authenticate_user!, only: %i[index new create show]
+ before_action :redirect_unauthorized_user, only: %i[index new create show]
+
+ def index
+ @advertisements = Advertisement.all
+ end
+
+ def new
+ @advertisement = Advertisement.new
+ end
+
+ def create
+ @advertisement = current_user.advertisements.build(ads_params)
+
+ redirect_to advertisement_path(@advertisement), notice: t('.success') if @advertisement.save
+ end
+
+ def show
+ @advertisement = Advertisement.find(params[:id])
+ end
+
+ def update
+ @advertisement = Advertisement.find(params[:id])
+ @advertisement.update(view_count: @advertisement.view_count + 1)
+
+ url = @advertisement.link
+ url = "http://#{url}" unless url.start_with?('http://', 'https://')
+
+ redirect_to url, allow_other_host: true
+ end
+
+ private
+
+ def ads_params
+ params.require(:advertisement).permit(:title, :link, :display_time, :image)
+ end
+
+ def redirect_unauthorized_user
+ redirect_to root_path unless current_user.admin?
+ end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index b5a0ba6..1583a1d 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -8,6 +8,6 @@ def index
return if @followed_posts.any?
- @posts = Post.get_sample(3)
+ @posts = Post.get_sample(10)
end
end
diff --git a/app/models/advertisement.rb b/app/models/advertisement.rb
new file mode 100644
index 0000000..ad58182
--- /dev/null
+++ b/app/models/advertisement.rb
@@ -0,0 +1,14 @@
+class Advertisement < ApplicationRecord
+ belongs_to :user
+ has_one_attached :image
+ validates :title, :link, presence: true
+ validates :link, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
+
+ def self.displayed
+ where.not(id: Advertisement.select(&:expired?)).sample(1)
+ end
+
+ def expired?
+ (created_at + display_time.days) < Time.zone.now
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b3e2455..05f5aa6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,6 +15,7 @@ class User < ApplicationRecord
has_many :professional_infos, through: :profile
has_many :education_infos, through: :profile
has_many :invitation_requests, through: :profile
+ has_many :advertisements, dependent: :destroy
has_one :subscription, dependent: :destroy
enum role: { user: 0, admin: 10 }
diff --git a/app/views/advertisements/_advertisement.html.erb b/app/views/advertisements/_advertisement.html.erb
new file mode 100644
index 0000000..0b57c24
--- /dev/null
+++ b/app/views/advertisements/_advertisement.html.erb
@@ -0,0 +1,7 @@
+
+ <%= button_to advertisement_path(advertisement), method: :patch,
+ class: 'advertisement rounded btn btn-light w-100', data: { turbo: false }, id: 'to-ad' do %>
+
<%= advertisement.title %>
+
<%= image_tag advertisement.image, width: '400rem', alt: 'advertisement' if advertisement.image.present? %>
+ <% end %>
+
diff --git a/app/views/advertisements/index.html.erb b/app/views/advertisements/index.html.erb
new file mode 100644
index 0000000..9678849
--- /dev/null
+++ b/app/views/advertisements/index.html.erb
@@ -0,0 +1,11 @@
+<%= link_to t('new_ad_btn'), new_advertisement_path, class: 'btn btn-primary mt-3' %>
+
+<% @advertisements.each do |ad| %>
+
+
+ -
+ <%= link_to ad.title, advertisement_path(ad) %>
+
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/advertisements/new.html.erb b/app/views/advertisements/new.html.erb
new file mode 100644
index 0000000..3557654
--- /dev/null
+++ b/app/views/advertisements/new.html.erb
@@ -0,0 +1,23 @@
+<%= form_with model: @advertisement, class: 'mt-3' do |f| %>
+
+ <%= f.label :title, class: 'form-label' %>
+ <%= f.text_field :title, class: 'form-control' %>
+
+
+
+ <%= f.label :link, class: 'form-label' %>
+ <%= f.text_field :link, class: 'form-control' %>
+
+
+
+ <%= f.label :display_time, class: 'form-label' %>
+ <%= f.number_field :display_time, class: 'form-control', min: 1 %>
+
+
+
+ <%= f.label :image, class: 'form-label' %>
+ <%= f.file_field :image, class: 'form-control' %>
+
+
+ <%= f.submit t('save_btn'), class: 'btn btn-primary mt-3' %>
+<% end %>
\ No newline at end of file
diff --git a/app/views/advertisements/show.html.erb b/app/views/advertisements/show.html.erb
new file mode 100644
index 0000000..05fea3e
--- /dev/null
+++ b/app/views/advertisements/show.html.erb
@@ -0,0 +1,14 @@
+<%= link_to t('return_btn'), advertisements_path, class: 'btn btn-secondary mb-3' %>
+
+
+
<%= @advertisement.title %>
+
+ <%= image_tag @advertisement.image, width: 400, class: 'mb-3' if @advertisement.image.present? %>
+
+
Tempo de exibição (em dias): <%= @advertisement.display_time %>
+
+
Link: <%= @advertisement.link %>
+
+
Cliques: <%= @advertisement.view_count %>
+
+
diff --git a/app/views/posts/_listing.html.erb b/app/views/posts/_listing.html.erb
index 5a85c5a..480d261 100644
--- a/app/views/posts/_listing.html.erb
+++ b/app/views/posts/_listing.html.erb
@@ -1,4 +1,4 @@
-<% posts.each do |post| %>
+<% posts.each_with_index do |post, index| %>
<%= link_to post, class: "text-decoration-none link-dark col-md-10" do %>
@@ -37,4 +37,7 @@
<% end %>
+ <% if current_user.subscription.inactive? && (index+1) % 5 == 0 && Advertisement.any? %>
+ <%= render partial: 'advertisements/advertisement', locals: { advertisement: Advertisement.displayed.first } %>
+ <% end %>
<% end %>
diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb
index 156a684..46462fe 100644
--- a/app/views/shared/_navbar.html.erb
+++ b/app/views/shared/_navbar.html.erb
@@ -69,6 +69,11 @@
<%= link_to t('settings.index.settings'), profile_settings_path(current_user.profile), class: 'nav-link' %>
+ <% if current_user.admin? %>
+
+ <%= link_to t('ads_btn'), advertisements_path, class: 'nav-link' %>
+
+ <% end %>
<%= link_to Subscription.model_name.human, subscriptions_path, class: 'nav-link text-primary' %>
diff --git a/config/locales/buttons.pt-BR.yml b/config/locales/buttons.pt-BR.yml
index b8949ae..9b88ca5 100644
--- a/config/locales/buttons.pt-BR.yml
+++ b/config/locales/buttons.pt-BR.yml
@@ -10,4 +10,6 @@ pt-BR:
unpin_btn: Desafixar
return_btn: Voltar
send_btn: Enviar
- publish_btn: Publicar
\ No newline at end of file
+ publish_btn: Publicar
+ ads_btn: Anúncios
+ new_ad_btn: Criar Anúncio
\ No newline at end of file
diff --git a/config/locales/models/advertisement.pt-BR.yml b/config/locales/models/advertisement.pt-BR.yml
new file mode 100644
index 0000000..8f902e2
--- /dev/null
+++ b/config/locales/models/advertisement.pt-BR.yml
@@ -0,0 +1,16 @@
+pt-BR:
+ activerecord:
+ models:
+ advertisement:
+ one: Anúncio
+ other: Anúncios
+ attributes:
+ advertisement:
+ title: Título
+ link: Link
+ image: Imagem
+ view_count: Visualizações
+ display_time: Prazo (em dias)
+ advertisements:
+ create:
+ success: Anúncio criado com sucesso
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index e779c33..e02d076 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,8 @@
root to: 'home#index'
+ resources :advertisements, only: %i[index show new create update]
+
resources :searches, only: %i[index]
resources :invitations, only: %i[index show] do
patch 'decline', on: :member
diff --git a/db/migrate/20240215195415_create_advertisements.rb b/db/migrate/20240215195415_create_advertisements.rb
new file mode 100644
index 0000000..b40a45e
--- /dev/null
+++ b/db/migrate/20240215195415_create_advertisements.rb
@@ -0,0 +1,13 @@
+class CreateAdvertisements < ActiveRecord::Migration[7.1]
+ def change
+ create_table :advertisements do |t|
+ t.string :link, null: false
+ t.integer :display_time, default: 0
+ t.integer :view_count, default: 0
+ t.string :title, null: false
+ t.references :user, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4463e2e..6aedd6d 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[7.1].define(version: 2024_02_15_181135) do
+ActiveRecord::Schema[7.1].define(version: 2024_02_15_195415) do
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
@@ -49,6 +49,17 @@
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
+ create_table "advertisements", force: :cascade do |t|
+ t.string "link", null: false
+ t.integer "display_time", default: 0
+ t.integer "view_count", default: 0
+ t.string "title", null: false
+ t.integer "user_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["user_id"], name: "index_advertisements_on_user_id"
+ end
+
create_table "billings", force: :cascade do |t|
t.integer "subscription_id", null: false
t.date "billing_date", null: false
@@ -411,6 +422,7 @@
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "advertisements", "users"
add_foreign_key "billings", "subscriptions"
add_foreign_key "comments", "posts"
add_foreign_key "comments", "users"
diff --git a/spec/factories/advertisements.rb b/spec/factories/advertisements.rb
new file mode 100644
index 0000000..243feb3
--- /dev/null
+++ b/spec/factories/advertisements.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :advertisement do
+ image { nil }
+ link { 'https://www.campuscode.com' }
+ display_time { 7 }
+ title { Faker::Lorem.paragraph }
+ user
+ end
+end
diff --git a/spec/models/advertisement_spec.rb b/spec/models/advertisement_spec.rb
new file mode 100644
index 0000000..504402e
--- /dev/null
+++ b/spec/models/advertisement_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe Advertisement, type: :model do
+ describe '#valid?' do
+ it 'Deve ter título' do
+ advertisement = Advertisement.new
+ advertisement.valid?
+ expect(advertisement.errors[:title]).to include('não pode ficar em branco')
+ end
+
+ context 'Formato do link' do
+ it 'Deve ter link' do
+ advertisement = Advertisement.new
+ advertisement.valid?
+ expect(advertisement.errors[:link]).to include('não pode ficar em branco')
+ end
+
+ it 'deve ser válido com formato correto' do
+ user = create(:user)
+ ad = Advertisement.new(title: 'Venha ser Dev', user:, link: 'https://www.campuscode.com')
+ expect(ad).to be_valid
+ end
+
+ it 'deve ser inválido com formato incorreto' do
+ user = create(:user)
+ ad = Advertisement.new(title: 'Venha ser Dev', user:, link: 'campus##code.com')
+ expect(ad).not_to be_valid
+ end
+ end
+ end
+
+ describe '#displayed' do
+ it 'deve retornar apenas anúncios não expirados' do
+ create(:advertisement, created_at: 6.days.ago, display_time: 7)
+
+ ads = Advertisement.displayed
+
+ expect(ads.size).to eq 1
+ end
+
+ it 'não deve retornar anúncios expirados' do
+ create(:advertisement, created_at: 2.days.ago, display_time: 1)
+
+ ads = Advertisement.displayed
+
+ expect(ads.size).to eq 0
+ end
+ end
+end
diff --git a/spec/requests/advertisements/user_visits_advertisements_page_spec.rb b/spec/requests/advertisements/user_visits_advertisements_page_spec.rb
new file mode 100644
index 0000000..afdb424
--- /dev/null
+++ b/spec/requests/advertisements/user_visits_advertisements_page_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+describe 'Usuário visita listagem de anúncios' do
+ it 'e não é admin' do
+ user = create(:user)
+
+ login_as user
+ get advertisements_path
+
+ expect(response).to redirect_to root_path
+ end
+
+ it 'e não está autenticado' do
+ get advertisements_path
+
+ expect(response).to redirect_to new_user_session_path
+ end
+end
diff --git a/spec/system/advertisements/admin_create_ad_spec.rb b/spec/system/advertisements/admin_create_ad_spec.rb
new file mode 100644
index 0000000..11c7b2f
--- /dev/null
+++ b/spec/system/advertisements/admin_create_ad_spec.rb
@@ -0,0 +1,39 @@
+require 'rails_helper'
+
+describe 'Administrador cadastra um anúncio' do
+ it 'com sucesso' do
+ admin = create(:user, role: 'admin')
+
+ login_as admin
+ visit root_path
+ click_button class: 'dropdown-toggle'
+ within 'nav' do
+ click_on 'Anúncios'
+ end
+ click_on 'Criar Anúncio'
+ fill_in 'Título', with: 'Buscador'
+ fill_in 'Link', with: 'https://www.google.com'
+ fill_in 'Prazo (em dias)', with: 7
+ attach_file('Imagem', Rails.root.join('spec/support/assets/images/test_image.png'))
+ click_on 'Salvar'
+
+ ad = Advertisement.last
+ expect(page).to have_content 'Anúncio criado com sucesso'
+ expect(page).to have_current_path advertisement_path(ad)
+ expect(page).to have_content 'Buscador'
+ expect(page).to have_css('img[src*="test_image.png"]')
+ expect(page).to have_link 'Voltar', href: advertisements_path
+ end
+
+ it 'e não é admin' do
+ user = create(:user)
+
+ login_as user
+ visit root_path
+ click_button class: 'dropdown-toggle'
+
+ within 'nav' do
+ expect(page).not_to have_link 'Anúncios'
+ end
+ end
+end
diff --git a/spec/system/advertisements/user_sees_ads_spec.rb b/spec/system/advertisements/user_sees_ads_spec.rb
new file mode 100644
index 0000000..fe55eec
--- /dev/null
+++ b/spec/system/advertisements/user_sees_ads_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+describe 'Usuário visualiza anúncios na home page' do
+ context 'com assinatura free' do
+ it 'a cada 5 posts' do
+ user = create(:user, :free)
+ admin = create(:user, role: 'admin')
+ ad1 = create(:advertisement, user: admin, title: 'Cursos de Software', link: 'https://campuscode.com.br')
+ ad2 = create(:advertisement, user: admin, title: 'Venha ser Dev', link: 'https://dev.com.br')
+
+ 10.times { create(:post) }
+ allow(Post).to receive(:get_sample).and_return(Post.all)
+ allow(Advertisement).to receive(:displayed).and_return([ad1], [ad2])
+
+ login_as user
+ visit root_path
+
+ within "#advertisement_#{ad1.id}" do
+ expect(page).to have_button 'Cursos de Software'
+ end
+ within "#advertisement_#{ad2.id}" do
+ expect(page).to have_button 'Venha ser Dev'
+ end
+
+ expect(page).to have_selector '.advertisement', count: 2
+ expect(page.body.index(Post.find(5).title)).to be < page.body.index('Cursos de Software')
+ expect(page.body.index(Post.find(6).title)).to be > page.body.index('Cursos de Software')
+ expect(page.body.index(Post.find(10).title)).to be < page.body.index('Venha ser Dev')
+ end
+
+ it 'e clica em um anúncio' do
+ user = create(:user, :free)
+ admin = create(:user, role: 'admin')
+ ad = create(:advertisement, user: admin, title: 'Cursos de Software', link: 'https://www.campuscode.com.br',
+ view_count: 0)
+ ad.image.attach(io: File.open('spec/support/assets/images/test_image.png'),
+ filename: 'test_image.png', content_type: 'image/png')
+ ad.save
+
+ 5.times { create(:post) }
+ allow(Post).to receive(:get_sample).and_return(Post.all)
+ login_as user
+ visit root_path
+
+ within "#advertisement_#{ad.id}" do
+ click_button id: 'to-ad'
+ end
+
+ expect(ad.reload.view_count).to eq 1
+ expect(page).to have_current_path ad.link
+ end
+ end
+
+ context 'com assinatura premium' do
+ it 'não visualiza anúncios' do
+ user = create(:user, :paid)
+ admin = create(:user, role: 'admin')
+ ad = create(:advertisement, user: admin, title: 'Cursos de Software', link: 'https://www.campuscode.com.br',
+ view_count: 0)
+ ad.image.attach(io: File.open('spec/support/assets/images/test_image.png'),
+ filename: 'test_image.png', content_type: 'image/png')
+ ad.save
+
+ 5.times { create(:post) }
+ allow(Post).to receive(:get_sample).and_return(Post.all)
+ login_as user
+ visit root_path
+
+ expect(page).not_to have_css "#advertisement_#{ad.id}"
+ expect(page).not_to have_content 'Cursos de Software'
+ expect(page).not_to have_link 'https://www.campuscode.com.br'
+ end
+ end
+end