Sponsorship: business logic (step 2) (#3440)

* Add Sponsorship model

* Add Sponsorship to admin

* Add correct relationships between models

* Add temporary script to migrate sponsorship data

* Use constants for enums

* Use Sponsorship model in PartnershipsController

* Use Sponsorship in the views

* Validate sponsorship levels and tag combos

* Replace the rest of the sponsorship code to use Sponsorship
This commit is contained in:
rhymes 2019-07-10 22:40:56 +02:00 committed by Ben Halpern
parent ad79116366
commit cecd1db070
20 changed files with 438 additions and 215 deletions

View file

@ -10,82 +10,82 @@ class PartnershipsController < ApplicationController
def show
skip_authorization
@organizations = current_user&.admin_organizations
end
def create
@level = params[:sponsorship_level]
@number_of_credits_needed = credits_for_level
@organization = Organization.find(params[:organization_id])
update_sponsorship_instructions
@organization = Organization.find(sponsorship_params[:organization_id])
authorize @organization, :admin_of_org?
@available_org_credits = @organization.credits.where(spent: false)
if @level == "tag"
@tag = Tag.find_by(name: params[:tag_name])
@tag.sponsor_organization_id = @organization.id
slackbot_ping("@#{current_user.username} bought a ##{@tag.name} sponsorship for @#{@organization.username}")
raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed
@tag.save!
spend_credits
redirect_back(fallback_location: "/partnerships")
elsif @level == "media"
# For now. Just ping slack.
slackbot_ping("📹 @#{current_user.username} bought #{@number_of_credits_needed} media credits for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
spend_credits
redirect_back(fallback_location: "/partnerships")
end
elsif @level == "devrel"
slackbot_ping("✍ @#{current_user.username} bought a devrel partnership for @#{@organization.username}")
if @available_org_credits.size >= @number_of_credits_needed
spend_credits
redirect_back(fallback_location: "/partnerships")
end
else
@organization.sponsorship_level = @level
@organization.sponsorship_status = "pending"
@organization.sponsorship_expires_at = (@organization.sponsorship_expires_at || Time.current) + 1.month
slackbot_ping("@#{current_user.username} bought a #{@level} sponsorship for @#{@organization.username}")
raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed
@organization.save!
spend_credits
redirect_back(fallback_location: "/partnerships")
@level = sponsorship_params[:sponsorship_level]
@number_of_credits_needed = Sponsorship::CREDITS[@level]
if @level == "media" && @number_of_credits_needed.nil?
@number_of_credits_needed = sponsorship_params[:sponsorship_amount].to_i
end
@available_org_credits = @organization.credits.unspent
# NOTE: this should probably be a redirect with a notice
raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed
tag_sponsorship = @level == "tag"
@tag = Tag.find_by!(name: sponsorship_params[:tag_name]) if tag_sponsorship
sponsorable = tag_sponsorship ? @tag : nil
purchase_sponsorship(
organization: @organization,
level: @level,
cost: @number_of_credits_needed,
sponsorable: sponsorable,
)
slackbot_ping(tag_sponsorship)
redirect_back(fallback_location: "/partnerships")
end
private
def credits_for_level
if @level == "gold"
6000
elsif @level == "silver"
500
elsif @level == "tag"
300
elsif @level == "bronze"
100
elsif @level == "devrel"
500
elsif @level == "media"
params[:sponsorship_amount].to_i
else
raise "Invalid level"
def sponsorship_params
allowed_params = %i[organization_id sponsorship_level sponsorship_amount tag_name sponsorship_instructions]
params.permit(allowed_params)
end
# NOTE: this should probably end up in a service object at some point
def purchase_sponsorship(organization:, level:, cost:, sponsorable: nil)
expires_at = Sponsorship::LEVELS_WITH_EXPIRATION.include?(level) ? 1.month.from_now : nil
create_params = {
user: current_user,
level: level,
status: :pending,
expires_at: expires_at
}
create_params[:sponsorable] = sponsorable if sponsorable
if sponsorship_params[:sponsorship_instructions]
create_params[:instructions] = sponsorship_params[:sponsorship_instructions]
# NOTE: why are we storing the updated_at of the instructions?
create_params[:instructions_updated_at] = Time.current
end
ActiveRecord::Base.transaction do
sponsorship = organization.sponsorships.create!(create_params)
Credits::Buyer.call(
purchaser: organization,
purchase: sponsorship,
cost: cost,
)
end
end
def update_sponsorship_instructions
@organization.sponsorship_instructions = @organization.sponsorship_instructions + "\n---\n#{Time.current}\n---\n" + params[:sponsorship_instructions].to_s
@organization.sponsorship_instructions_updated_at = Time.current
end
def slackbot_ping(tag_sponsorship)
text = if tag_sponsorship
"@#{current_user.username} bought a ##{@tag.name} sponsorship for @#{@organization.username}"
else
"@#{current_user.username} bought a #{@level} sponsorship for @#{@organization.username}"
end
def spend_credits
@available_org_credits.limit(@number_of_credits_needed).update_all(spent: true)
end
def slackbot_ping(text)
SlackBot.ping(
text,
channel: "incoming-partners",

View file

@ -6,7 +6,7 @@ class TagsController < ApplicationController
def index
skip_authorization
@tags_index = true
@tags = Tag.includes(:sponsor_organization).order("hotness_score DESC").limit(100)
@tags = Tag.includes(:sponsorship).order(hotness_score: :desc).limit(100)
end
def edit

View file

@ -25,11 +25,4 @@ class OrganizationDecorator < ApplicationDecorator
text: "#ffffff"
}
end
def sponsorship_color_hex
hexes = { "gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);",
"silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);",
"bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);" }
hexes[sponsorship_level]
end
end

View file

@ -0,0 +1,12 @@
class SponsorshipDecorator < ApplicationDecorator
delegate_all
def level_color_hex
hexes = {
"gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);",
"silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);",
"bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);"
}
hexes[level]
end
end

View file

@ -195,20 +195,4 @@ module ApplicationHelper
def community_qualified_name
"The #{ApplicationConfig['COMMUNITY_NAME']} Community"
end
def sponsorship_credits_price(level)
if level == "bronze"
100
elsif level == "silver"
500
elsif level == "gold"
4000
elsif level == "tag"
300
elsif level == "media"
25
elsif level == "devrel"
500
end
end
end

View file

@ -1,10 +1,44 @@
class Sponsorship < ApplicationRecord
LEVELS = %w[gold silver bronze tag media devrel].freeze
LEVELS_WITH_EXPIRATION = %w[gold silver bronze].freeze
STATUSES = %w[none pending live].freeze
# media has no fixed amount of credits
CREDITS = {
gold: 6_000,
silver: 500,
bronze: 100,
tag: 300,
devrel: 500
}.with_indifferent_access.freeze
belongs_to :user
belongs_to :organization, inverse_of: :sponsorships
belongs_to :sponsorable, polymorphic: true, optional: true
validates :user, :organization, :featured_number, presence: true
validates :level, inclusion: { in: %w[gold silver bronze tag media devrel] }
validates :status, inclusion: { in: %w[none pending live] }
validates :level, inclusion: { in: LEVELS }
validates :status, inclusion: { in: STATUSES }
validates :url, url: { allow_blank: true, no_local: true, schemes: %w[http https] }
validates :level,
uniqueness: {
scope: :organization,
message: "You can have only one sponsorship of #{LEVELS_WITH_EXPIRATION}"
},
if: proc { LEVELS_WITH_EXPIRATION.include?(level) }
validates :level,
uniqueness: {
scope: [:sponsorable],
message: proc { "The tag is already sponsored" }
},
if: proc { level.to_s == "tag" }
scope :gold, -> { where(level: :gold) }
scope :silver, -> { where(level: :silver) }
scope :bronze, -> { where(level: :bronze) }
scope :tag, -> { where(level: :tag) }
scope :media, -> { where(level: :media) }
scope :devrel, -> { where(level: :devrel) }
scope :live, -> { where(status: :live) }
scope :pending, -> { where(status: :pending) }
end

View file

@ -19,6 +19,7 @@ class Tag < ActsAsTaggableOn::Tag
belongs_to :badge, optional: true
belongs_to :sponsor_organization, class_name: "Organization", optional: true
has_one :sponsorship, as: :sponsorable, inverse_of: :sponsorable, dependent: :destroy
mount_uploader :profile_image, ProfileImageUploader
mount_uploader :social_image, ProfileImageUploader

View file

@ -10,11 +10,11 @@
<img src="<%= asset_path "emoji/emoji-one-heart.png" %>" alt="emoji heart" />
</span>
</header>
<% @sponsors = Organization.where(sponsorship_level: "gold", sponsorship_status: "live").order("sponsorship_featured_number ASC") %>
<% if @sponsors.any? %>
<% @sponsorships = Sponsorship.gold.live.includes(:organization).order(featured_number: :asc) %>
<% if @sponsorships.any? %>
<div class="widget-body">
<% @sponsors.each do |organization| %>
<%= render "articles/single_sponsor", organization: organization %>
<% @sponsorships.find_each do |sponsorship| %>
<%= render "articles/single_sponsor", sponsorship: sponsorship %>
<% end %>
<div class="sponsors-love-message">
We are grateful for wonderful sponsors who help sustain the <%= ApplicationConfig["COMMUNITY_NAME"] %> community.

View file

@ -1,11 +1,12 @@
<% organization = sponsorship.organization %>
<div class="sidebar-sponsor">
<a class="partner-link" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__image">
<a class="partner-link" href="<%= sponsorship.url %>" target="_blank" data-details="<%= organization.slug %>__image">
<img src="<%= cloudinary(organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-light-mode" />
<img src="<%= cloudinary(organization.dark_nav_image_url || organization.nav_image_url) %>" style="margin-bottom:8px;" alt="<%= organization.name %> logo" loading="lazy" class="partner-image-dark-mode" />
</a>
<div class="sponsor-tagline" style="display:inline;">
<%= organization.sponsorship_tagline %>
<a class="partner-link sponsor-learn-more" href="<%= organization.sponsorship_url %>" target="_blank" data-details="<%= organization.slug %>__learn-more">
<%= sponsorship.tagline %>
<a class="partner-link sponsor-learn-more" href="<%= sponsorship.url %>" target="_blank" data-details="<%= organization.slug %>__learn-more">
Learn More
</a>
</div>

View file

@ -35,13 +35,13 @@
</div>
</div>
<% end %>
<% if @tag_model.sponsor_organization_id && @tag_model.sponsorship_status == "live" %>
<% if @tag_model.sponsorship && @tag_model.sponsorship.status == "live" %>
<div class="widget">
<header>
<h4><%= @tag %> is sponsored by</h4>
</header>
<div class="widget-body">
<%= render "articles/single_sponsor", organization: @tag_model.sponsor_organization %>
<%= render "articles/single_sponsor", sponsorship: @tag_model.sponsorship %>
</div>
</div>
<% end %>

View file

@ -1,17 +1,18 @@
<div id="sidebar-wrapper-right" class="sidebar-wrapper sidebar-wrapper-right">
<div class="sidebar-bg" id="sidebar-bg-right"></div>
<div class="side-bar sidebar-additional showing" id="sidebar-additional">
<% if @organization.sponsorship_status == "live" %>
<div class="sidebar-sponsorship-level" style="background:<%= @organization.decorate.sponsorship_color_hex %>">
<%= @organization.sponsorship_level.capitalize %> Community Sponsor ❤️
<% @organization.sponsorships.live.find_each do |sponsorship| %>
<div class="sidebar-sponsorship-level" style="background:<%= sponsorship.decorate.level_color_hex %>">
<%= sponsorship.level.capitalize %> Community Sponsor ❤️
</div>
<% end %>
<% if @organization.users.present? %>
<div class="widget">
<div class="widget-suggested-follows-container">
<header><h4>meet the team</h4></header>
<div class="widget-body">
<% @organization.users.each do |user| %>
<% @organization.users.find_each do |user| %>
<div class="widget-user-pic">
<a href="/<%= user.username %>">
<img src="<%= ProfileImage.new(user).get(90) %>" alt="<%= user.username %> profile image">

View file

@ -4,7 +4,6 @@
<link rel="canonical" href="https://dev.to/sponsors" />
<meta name="description" content="dev.to | Sponsors">
<meta name="keywords" content="software development,engineering,rails,javascript,ruby">
<% end %>
<div class="blank-space"></div>
@ -36,12 +35,11 @@
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="golden badge">
</h4>
<div id="gold-sponsors">
<% Organization.where(sponsorship_level: "gold").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<% Sponsorship.gold.includes(:organization).order(featured_number: :asc).find_each do |sponsorship| %>
<a href="<%= sponsorship.url %>"><img src="<%= cloudinary(sponsorship.organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
<%= sponsorship.blurb_html.html_safe %>
</p>
<hr />
<% end %>
</div>
@ -52,10 +50,10 @@
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="silver badge">
</h4>
<div id="silver-sponsors">
<% Organization.where(sponsorship_level: "silver").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<% Sponsorship.silver.includes(:organization).order(featured_number: :asc).find_each do |sponsorship| %>
<a href="<%= sponsorship.url %>"><img src="<%= cloudinary(sponsorship.organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
<%= sponsorship.blurb_html.html_safe %>
</p>
<hr />
<% end %>
@ -67,15 +65,15 @@
<img src="<%= cloudinary("https://thepracticaldev.s3.amazonaws.com/i/2zmukvll98je8qsdyewq.png", 30) %>" alt="bronze badge">
</h4>
<div id="bronze-sponsors">
<% Organization.where(sponsorship_level: "bronze").order("sponsorship_featured_number ASC").each do |organization| %>
<a href="<%= organization.sponsorship_url %>"><img src="<%= cloudinary(organization.nav_image_url, 250) %>"></a>
<% Sponsorship.bronze.includes(:organization).order(featured_number: :asc).find_each do |sponsorship| %>
<a href="<%= sponsorship.url %>"><img src="<%= cloudinary(sponsorship.organization.nav_image_url, 250) %>"></a>
<p>
<%= organization.sponsorship_blurb_html.html_safe %>
<%= sponsorship.blurb_html.html_safe %>
</p>
<hr />
<% end %>
</div>
<h5> Interested in sponsoring dev.to? Email <a href="mailto:peter@dev.to">peter@dev.to</a></h5>
<h5>Interested in sponsoring dev.to? Email <a href="mailto:peter@dev.to">peter@dev.to</a></h5>
</div>
</div>

View file

@ -1,6 +1,4 @@
<% org = current_user.admin_organizations.first %>
<% if org == nil %>
<% if !organizations.any? %>
<div class="partner-credits-explainer">
<h4>What next?</h4>
<h3>Create an Organization</h3>
@ -13,13 +11,15 @@
<a href="/settings/organization">Create Organization</a>
</div>
<% elsif %w[silver bronze tag].include?(level) %>
<% current_user.admin_organizations.each do |org| %>
<% if org.credits.where(spent: false).size < sponsorship_credits_price(level) %>
<% organizations.find_each do |org| %>
<% if org.credits.unspent.size < Sponsorship::CREDITS[level] %>
<div class="partner-credits-explainer">
<h4>What next?</h4>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" /> Purchase Credits for @<%= org.slug %></h3>
<% if org.credits.where(spent: false).size > 0 %>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<% if org.credits.unspent.size > 0 %>
<div style="font-size: 0.88em;">
<em>This organization account has <%= org.credits.unspent.size %> credits available</em>
</div>
<% end %>
<p>
<em>Credits are your wallet for flexibly managing all your DEV partnerships.</em>
@ -34,19 +34,24 @@
</div>
<% else %>
<div class="partner-credits-explainer">
<% if %w[gold silver bronze].include?(level) %>
<% if Sponsorship::LEVELS_WITH_EXPIRATION.include?(level) %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<% if org.sponsorship_level == level %>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.unspent.size %> credits available</em></div>
<% sponsorships = org.sponsorships.where(level: Sponsorship::LEVELS_WITH_EXPIRATION) %>
<% level_sponsorship = sponsorships.where(level: level).take %>
<% if level_sponsorship %>
<div class="partner-explainer-notice">
🎉 You are Subscribed as a <%= level.capitalize %> Sponsor.
<% if org.sponsorship_status == "pending" %>
<% if level_sponsorship.status == "pending" %>
<br /><br />Your placement is <em>pending</em>.
<% end %>
</div>
<% elsif org.sponsorship_level.present? %>
<% elsif sponsorships.any? %>
<div class="partner-explainer-notice">
🛑 You are already subscribed as a <%= org.sponsorship_level %> sponsor. Contact partnerships@dev.to to change plans.
<%# this should never happen but better safe than sorry %>
<% sponsorships.each do |sponsorship| %>
🛑 You are already subscribed as a <%= sponsorship.level %> sponsor. Contact partnerships@dev.to to change plans.
<% end %>
</div>
<% else %>
<h2><%= level.capitalize %> Sponsor Subscription</h2>
@ -55,7 +60,7 @@
<%= hidden_field_tag(:sponsorship_level, level) %>
<h4>Sponsorship message/brand instructions:</h4>
<%= text_area_tag(:sponsorship_instructions, nil, placeholder: "Include brand guideline links and detailed instructions about the message you are trying to get across. DEV editors will use these guidelines contextually depending on sponsorship.") %>
<button>Subscribe for <%= sponsorship_credits_price(level) %> credits</button>
<button>Subscribe for <%= Sponsorship::CREDITS[level] %> credits</button>
<p style="font-size: 0.88em;">
<em>
This subscription will renew every month with credits in your account. If there are no credits, 50 credits will be purchased at the appropriate price. You can cancel this automatic sponsorship subscription at any time.
@ -65,32 +70,35 @@
<% end %>
<% elsif level == "tag" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.unspent.size %> credits available</em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
<%= hidden_field_tag(:sponsorship_level, level) %>
<h4>Sponsorship message/brand instructions:</h4>
<%= text_area_tag(:sponsorship_instructions, nil, placeholder: "Include brand guideline links and detailed instructions about the message you are trying to get across. DEV editors will use these guidelines contextually depending on sponsorship.") %>
<% Tag.where(sponsor_organization_id: org.id).each do |tag| %>
<% org.sponsorships.where(level: :tag).find_each do |sponsorship| %>
<div class="partner-explainer-notice">
🎉 You are Subscribed as the sponsor of #<%= tag.name %>.
🎉 You are Subscribed as the sponsor of #<%= sponsorship.sponsorable.name %>.
</div>
<% end %>
<% if Tag.where(sponsor_organization_id: org.id).any? %>
<% if org.sponsorships.where(level: :tag).any? %>
<h4>Sponsor another tag:</h4>
<% end %>
<%= select_tag "tag_name", options_for_select(Tag.where(sponsor_organization_id: nil, supported: true).pluck(:name)) %>
<button>Sponsor Tag for 300 credits</button>
<% sponsored_tags = org.sponsorships.includes(:sponsorable).where(level: :tag).map { |sp| sp.sponsorable.name } %>
<% tag_names = Tag.where(supported: true).where.not(name: sponsored_tags).pluck(:name) %>
<%= select_tag "tag_name", options_for_select(tag_names) %>
<button>Sponsor Tag for <%= Sponsorship::CREDITS[:tag] %> credits</button>
<p style="font-size: 0.88em;">
<em>
This subscription will renew every month with credits in your account. If there are no credits, 50 credits will be purchased at the appropriate price. You can cancel this automatic sponsorship subscription at any time.
</em>
</p>
<% end %>
<%# NOTE: this currently can't be reached %>
<% elsif level == "media" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.unspent.size %> credits available</em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>
@ -103,9 +111,10 @@
</em>
</p>
<% end %>
<%# NOTE: this currently can't be reached %>
<% elsif level == "devrel" %>
<h3><img src="<%= ProfileImage.new(org).get(90) %>" />@<%= org.slug %></h3>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.where(spent: false).size %> credits available </em></div>
<div style="font-size: 0.88em;"><em>This organization account has <%= org.credits.unspent.size %> credits available</em></div>
<br />
<%= form_tag "/partnerships" do %>
<%= hidden_field_tag(:organization_id, org.id) %>

View file

@ -22,7 +22,7 @@
<%= render "devrel_consult_copy" %>
<% end %>
<% if user_signed_in? %>
<%= render "form", level: params[:option].split("-")[0] %>
<%= render "form", level: params[:option].split("-")[0], organizations: @organizations %>
<% else %>
<div class="partner-credits-explainer">
<a href="/enter">Sign in to get started</a>

View file

@ -33,8 +33,8 @@
<div class="tag-list-container" style="background:<%= tag.bg_color_hex %>;">
<a class="tag-show-link" href="/t/<%= tag.name %>" style="color:<%= tag.text_color_hex || "#fff" %>;">
<h2>#<%= tag.name %></h2>
<% if tag.sponsor_organization && tag.sponsorship_status == "live" %>
<h4>Sponsored by <%= tag.sponsor_organization.name %> ❤️</h4>
<% if tag.sponsorship && tag.sponsorship.status == "live" %>
<h4>Sponsored by <%= tag.sponsorship.organization.name %> ❤️</h4>
<% end %>
</a>
</div>

View file

@ -153,7 +153,7 @@ Rails.application.routes.draw do
resources :poll_votes, only: %i[show create]
resources :poll_skips, only: [:create]
resources :profile_pins, only: %i[create update]
resources :partnerships, only: %i[index create]
resources :partnerships, only: %i[index create show], param: :option
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/listings/dashboard" => "classified_listings#dashboard"
@ -174,7 +174,6 @@ Rails.application.routes.draw do
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
get "/live/:username" => "twitch_live_streams#show"
get "/partnerships/:option" => "partnerships#show"
get "/pro" => "pro_accounts#index"
post "/pusher/auth" => "pusher#auth"

View file

@ -6,14 +6,24 @@ RSpec.describe Sponsorship, type: :model do
it { is_expected.to belong_to(:sponsorable).optional }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:organization) }
it { is_expected.to validate_inclusion_of(:level).in_array(%w[gold silver bronze tag media devrel]) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w[none pending live]) }
it { is_expected.to validate_inclusion_of(:level).in_array(Sponsorship::LEVELS) }
it { is_expected.to validate_inclusion_of(:status).in_array(Sponsorship::STATUSES) }
it { is_expected.to allow_values(nil).for(:expires_at) }
it { is_expected.not_to allow_values(nil).for(:featured_number) }
it { is_expected.to have_db_index(:level) }
it { is_expected.to have_db_index(:status) }
it { is_expected.to have_db_index(%i[sponsorable_id sponsorable_type]) }
describe "constants" do
it "has the correct values for constants" do
expect(Sponsorship::LEVELS).to eq(%w[gold silver bronze tag media devrel])
expect(Sponsorship::LEVELS_WITH_EXPIRATION).to eq(%w[gold silver bronze])
expect(Sponsorship::STATUSES).to eq(%w[none pending live])
expected_credits = { gold: 6000, silver: 500, bronze: 100, tag: 300, devrel: 500 }.with_indifferent_access
expect(Sponsorship::CREDITS).to eq(expected_credits)
end
end
describe "#url" do
let(:sponsorship) { build(:sponsorship) }
@ -37,4 +47,37 @@ RSpec.describe Sponsorship, type: :model do
expect(sponsorship).not_to be_valid
end
end
describe "validations" do
let(:org) { create(:organization) }
it "forbids an org to have multiple 'expiring' sponsorships" do
create(:sponsorship, level: :gold, organization: org)
bronze_sponsorship = build(:sponsorship, level: :gold, organization: org)
expect(bronze_sponsorship).not_to be_valid
end
context "when tag sponsorships" do
it "allows an org to have multiple tag sponsorships on different tags" do
create(:sponsorship, level: :tag, organization: org, sponsorable: create(:tag, name: "python"))
tag_sponsorship = build(:sponsorship, level: :tag, organization: org, sponsorable: create(:tag, name: "ruby"))
expect(tag_sponsorship).to be_valid
end
it "forbids an org to have multiple tag sponsorships on the same tag" do
tag = create(:tag, name: "python")
create(:sponsorship, level: :tag, organization: org, sponsorable: tag)
tag_sponsorship = build(:sponsorship, level: :tag, organization: org, sponsorable: tag)
expect(tag_sponsorship).not_to be_valid
end
it "forbids different orgs to have a sponsorship on the same tag" do
tag = create(:tag, name: "python")
other_org = create(:organization)
create(:sponsorship, level: :tag, organization: org, sponsorable: tag)
tag_sponsorship = build(:sponsorship, level: :tag, organization: other_org, sponsorable: tag)
expect(tag_sponsorship).not_to be_valid
end
end
end
end

View file

@ -1,3 +1,4 @@
# rubocop:disable RSpec/NestedGroups
require "rails_helper"
RSpec.describe "Pages", type: :request do
@ -70,88 +71,225 @@ RSpec.describe "Pages", type: :request do
describe "POST /partnerships" do
let(:user) { create(:user) }
let(:organization) { create(:organization) }
let(:org) { create(:organization) }
context "when user is logged in as an admin and has enough credits" do
before do
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "admin")
Credit.add_to_org(organization, 2000)
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
sign_in user
end
it "subscribes to bronze sponsorship" do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("bronze")
# context "when purchasing a gold sponsorship" is skipped due
# to the high amount of required credits
context "when purchasing a silver sponsorship" do
let(:params) { { sponsorship_level: :silver, organization_id: org.id } }
before do
Credit.add_to_org(org, Sponsorship::CREDITS[:silver])
end
it "creates a new sponsorship" do
expect do
post "/partnerships", params: params
expect(response).to redirect_to(partnerships_path)
end.to change(org.sponsorships, :count).by(1)
end
it "subscribes with the correct info" do
Timecop.freeze(Time.current) do
post "/partnerships", params: params
sponsorship = org.sponsorships.silver.last
expect(sponsorship.status).to eq("pending")
expect(sponsorship.expires_at.to_i).to eq(1.month.from_now.to_i)
expect(sponsorship.sponsorable).to be(nil)
expect(sponsorship.instructions).to be_blank
expect(sponsorship.instructions_updated_at).to be(nil)
end
end
it "detracts the correct amount of credits" do
expect do
post "/partnerships", params: params
end.to change(org.credits.spent, :size).by(Sponsorship::CREDITS[:silver])
credit = org.credits.spent.last
expect(credit.purchase.is_a?(Sponsorship)).to be(true)
end
end
it "subscribes to silver sponsorship" do
post "/partnerships", params: {
sponsorship_level: "silver",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("silver")
context "when purchasing a bronze sponsorship" do
let(:params) { { sponsorship_level: :bronze, organization_id: org.id } }
before do
Credit.add_to_org(org, Sponsorship::CREDITS[:bronze])
end
it "creates a new sponsorship" do
expect do
post "/partnerships", params: params
expect(response).to redirect_to(partnerships_path)
end.to change(org.sponsorships, :count).by(1)
end
it "subscribes with the correct info" do
Timecop.freeze(Time.current) do
post "/partnerships", params: params
sponsorship = org.sponsorships.bronze.last
expect(sponsorship.status).to eq("pending")
expect(sponsorship.expires_at.to_i).to eq(1.month.from_now.to_i)
expect(sponsorship.sponsorable).to be(nil)
expect(sponsorship.instructions).to be_blank
expect(sponsorship.instructions_updated_at).to be(nil)
end
end
it "detracts the correct amount of credits" do
expect do
post "/partnerships", params: params
end.to change(org.credits.spent, :size).by(Sponsorship::CREDITS[:bronze])
credit = org.credits.spent.last
expect(credit.purchase.is_a?(Sponsorship)).to be(true)
end
end
xit "subscribes to gold sponsorship" do # skipped for now do to high credit need (not sure making this self serve makes sense)
post "/partnerships", params: {
sponsorship_level: "gold",
organization_id: organization.id
}
expect(organization.reload.sponsorship_level).to eq("gold")
context "when purchasing a devrel sponsorship" do
let(:params) { { sponsorship_level: :devrel, organization_id: org.id } }
before do
Credit.add_to_org(org, Sponsorship::CREDITS[:devrel])
end
it "creates a new sponsorship" do
expect do
post "/partnerships", params: params
expect(response).to redirect_to(partnerships_path)
end.to change(org.sponsorships, :count).by(1)
end
it "subscribes with the correct info" do
Timecop.freeze(Time.current) do
post "/partnerships", params: params
sponsorship = org.sponsorships.devrel.last
expect(sponsorship.status).to eq("pending")
expect(sponsorship.expires_at).to be(nil)
expect(sponsorship.sponsorable).to be(nil)
expect(sponsorship.instructions).to be_blank
expect(sponsorship.instructions_updated_at).to be(nil)
end
end
it "detracts the correct amount of credits" do
expect do
post "/partnerships", params: params
end.to change(org.credits.spent, :size).by(Sponsorship::CREDITS[:devrel])
credit = org.credits.spent.last
expect(credit.purchase.is_a?(Sponsorship)).to be(true)
end
end
it "subscribes to editorial subscription" do
post "/partnerships", params: {
sponsorship_level: "devrel",
organization_id: organization.id
}
expect(organization.reload.credits.where(spent: false).size).to eq(1500)
context "when purchasing a media sponsorship" do
let(:params) do
{ sponsorship_level: :media, organization_id: org.id, sponsorship_amount: 10 }
end
before do
Credit.add_to_org(org, params[:sponsorship_amount])
end
it "creates a new sponsorship" do
expect do
post "/partnerships", params: params
expect(response).to redirect_to(partnerships_path)
end.to change(org.sponsorships, :count).by(1)
end
it "subscribes with the correct info" do
Timecop.freeze(Time.current) do
post "/partnerships", params: params
sponsorship = org.sponsorships.media.last
expect(sponsorship.status).to eq("pending")
expect(sponsorship.expires_at).to be(nil)
expect(sponsorship.sponsorable).to be(nil)
expect(sponsorship.instructions).to be_blank
expect(sponsorship.instructions_updated_at).to be(nil)
end
end
it "detracts the correct amount of credits" do
expect do
post "/partnerships", params: params
end.to change(org.credits.spent, :size).by(params[:sponsorship_amount])
credit = org.credits.spent.last
expect(credit.purchase.is_a?(Sponsorship)).to be(true)
end
end
it "subscribes to media sponsorship" do
post "/partnerships", params: {
sponsorship_level: "media",
organization_id: organization.id,
sponsorship_amount: 900
}
expect(organization.reload.credits.where(spent: false).size).to eq(1100)
context "when purchasing a tag sponsorship" do
let(:tag) { create(:tag) }
let(:params) { { sponsorship_level: :tag, organization_id: org.id, tag_name: tag.name } }
before do
Credit.add_to_org(org, Sponsorship::CREDITS[:tag])
end
it "creates a new sponsorship" do
expect do
post "/partnerships", params: params
expect(response).to redirect_to(partnerships_path)
end.to change(org.sponsorships, :count).by(1)
end
it "subscribes with the correct info" do
Timecop.freeze(Time.current) do
post "/partnerships", params: params
sponsorship = org.sponsorships.tag.last
expect(sponsorship.status).to eq("pending")
expect(sponsorship.expires_at).to be(nil)
expect(sponsorship.sponsorable).not_to be(nil)
expect(sponsorship.instructions).to be_blank
expect(sponsorship.instructions_updated_at).to be(nil)
end
end
it "detracts the correct amount of credits" do
expect do
post "/partnerships", params: params
end.to change(org.credits.spent, :size).by(Sponsorship::CREDITS[:tag])
credit = org.credits.spent.last
expect(credit.purchase.is_a?(Sponsorship)).to be(true)
end
end
it "subscribes to tag sponsorship" do
tag = create(:tag)
it "updates sponsorship instructions if present" do
Credit.add_to_org(org, Sponsorship::CREDITS[:bronze])
post "/partnerships", params: {
sponsorship_level: "tag",
organization_id: organization.id,
sponsorship_amount: 900,
tag_name: tag.name
}
expect(organization.reload.credits.where(spent: false).size).to eq(1700)
expect(tag.reload.sponsor_organization_id).to eq(organization.id)
end
it "updates sponsorship instructions if new instructions" do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id,
sponsorship_level: :bronze,
organization_id: org.id,
sponsorship_instructions: "hello there"
}
expect(organization.reload.sponsorship_instructions).to include("hello there")
expect(organization.reload.sponsorship_instructions_updated_at).to be > 30.seconds.ago
sponsorship = org.sponsorships.bronze.last
expect(sponsorship.instructions).to include("hello there")
expect(sponsorship.instructions_updated_at).not_to be(nil)
end
end
context "when user is logged in as a non-admin" do
context "when user is logged in as a non organization admin but has enough credits" do
before do
OrganizationMembership.create(user_id: user.id, organization_id: organization.id, type_of_user: "member")
Credit.add_to_org(organization, 2000)
create(:organization_membership, user: user, organization: org, type_of_user: "member")
Credit.add_to_org(org, Sponsorship::CREDITS[:bronze])
sign_in user
end
it "subscribes to bronze sponsorship" do
it "does not subscribe to a bronze sponsorship" do
expect do
post "/partnerships", params: {
sponsorship_level: "bronze",
organization_id: organization.id
organization_id: org.id
}
end.to raise_error(Pundit::NotAuthorizedError)
end
end
end
end
# rubocop:enable RSpec/NestedGroups

View file

@ -41,16 +41,25 @@ RSpec.describe "StoriesIndex", type: :request do
expect(response.body).not_to include(ad.processed_html)
end
it "has sponsors displayed" do
org = create(:organization, sponsorship_level: "gold", sponsorship_tagline: "Oh Yeah!!!", sponsorship_status: "live")
it "has gold sponsors displayed" do
org = create(:organization)
sponsorship = create(:sponsorship, level: "gold", tagline: "Oh Yeah!!!", status: "live", organization: org)
get "/"
expect(response.body).to include(org.sponsorship_tagline)
expect(response.body).to include(sponsorship.tagline)
end
it "does not display non-sponsor org" do
org = create(:organization, sponsorship_level: nil, sponsorship_tagline: "Oh Yeah!!!")
it "does not display silver sponsors" do
org = create(:organization)
sponsorship = create(:sponsorship, level: "silver", tagline: "Oh Yeah!!!", status: "live", organization: org)
get "/"
expect(response.body).not_to include(org.sponsorship_tagline)
expect(response.body).not_to include(sponsorship.tagline)
end
it "does not display non live gold sponsorships" do
org = create(:organization)
sponsorship = create(:sponsorship, level: "gold", tagline: "Oh Yeah!!!", status: "pending", organization: org)
get "/"
expect(response.body).not_to include(sponsorship.tagline)
end
it "shows listings" do
@ -77,14 +86,14 @@ RSpec.describe "StoriesIndex", type: :request do
end
describe "GET tag index" do
let(:tag) { create(:tag) }
it "renders page with proper header" do
tag = create(:tag)
get "/t/#{tag.name}"
expect(response.body).to include(tag.name)
end
it "renders page with top/week etc." do
tag = create(:tag)
get "/t/#{tag.name}/top/week"
expect(response.body).to include(tag.name)
get "/t/#{tag.name}/top/month"
@ -96,26 +105,27 @@ RSpec.describe "StoriesIndex", type: :request do
end
it "renders tag after alias change" do
tag = create(:tag)
tag2 = create(:tag, alias_for: tag.name)
get "/t/#{tag2.name}"
expect(response.body).to redirect_to "/t/#{tag.name}"
end
it "does not render sponsor if not live" do
organization = create(:organization, sponsorship_tagline: "Oh Yeah!!!")
tag = create(:tag, sponsor_organization_id: organization.id)
org = create(:organization)
sponsorship = create(:sponsorship, level: :tag, tagline: "Oh Yeah!!!", status: "pending", organization: org, sponsorable: tag)
get "/t/#{tag.name}"
expect(response.body).not_to include("is sponsored by")
expect(response.body).not_to include(organization.sponsorship_tagline)
expect(response.body).not_to include(sponsorship.tagline)
end
it "renders sponsor" do
organization = create(:organization, sponsorship_tagline: "Oh Yeah!!!!!!")
tag = create(:tag, sponsor_organization_id: organization.id, sponsorship_status: "live")
it "renders live sponsor" do
org = create(:organization)
sponsorship = create(:sponsorship, level: :tag, tagline: "Oh Yeah!!!", status: "live", organization: org, sponsorable: tag)
get "/t/#{tag.name}"
expect(response.body).to include("is sponsored by")
expect(response.body).to include(organization.sponsorship_tagline)
expect(response.body).to include(sponsorship.tagline)
end
end
end

View file

@ -57,8 +57,8 @@ RSpec.describe "UserProfiles", type: :request do
end
it "renders sponsor if it is sponsored" do
organization.update_columns(sponsorship_level: "gold", sponsorship_status: "live")
get organization.reload.path
create(:sponsorship, level: :gold, status: :live, organization: organization)
get organization.path
expect(response.body).to include "Gold Community Sponsor"
end
end