Use new classified listing categories (#7250)

* Add ClassifiedListingCategoryModel

* Add data update script for classified listing categories

* Refactor and fix specs

* Resolve conflict

* Incorporate PR feedback
This commit is contained in:
Michael Kohl 2020-04-23 10:06:58 +07:00 committed by GitHub
parent 046ec2eaee
commit 9df68f3920
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 261 additions and 181 deletions

View file

@ -16,10 +16,13 @@ module Api
def index
@classified_listings = ClassifiedListing.published.
select(ATTRIBUTES_FOR_SERIALIZATION).
includes(:user, :organization, :taggings)
@classified_listings = @classified_listings.where(category: params[:category]) if params[:category].present?
includes(:user, :organization, :taggings, :classified_listing_category)
if params[:category].present?
category = ClassifiedListingCategory.find_by(slug: params[:category])
@classified_listings =
@classified_listings.where(classified_listing_category: category)
end
@classified_listings = @classified_listings.order(bumped_at: :desc)
per_page = (params[:per_page] || 30).to_i
@ -51,8 +54,8 @@ module Api
end
ATTRIBUTES_FOR_SERIALIZATION = %i[
id user_id organization_id title slug body_markdown
cached_tag_list category processed_html published
id user_id organization_id title slug body_markdown cached_tag_list
category classified_listing_category_id category processed_html published
].freeze
private_constant :ATTRIBUTES_FOR_SERIALIZATION

View file

@ -16,14 +16,15 @@ class ClassifiedListingsController < ApplicationController
return redirect_to "/internal/listings/#{@displayed_classified_listing.id}/edit"
end
@classified_listings = if params[:category].blank?
published_listings.
order("bumped_at DESC").
includes(:user, :organization, :taggings).
limit(12)
else
ClassifiedListing.none
end
@classified_listings =
if params[:category].blank?
published_listings.
order("bumped_at DESC").
includes(:user, :organization, :taggings, :classified_listing_category).
limit(12)
else
ClassifiedListing.none
end
set_surrogate_key_header "classified-listings-#{params[:category]}"
end

View file

@ -13,11 +13,14 @@ module ClassifiedListingsToolkit
@classified_listing.save
end
# TODO: why not just @classified_listing.update(listing_params)?
def update_listing_details
@classified_listing.title = listing_params[:title] if listing_params[:title]
@classified_listing.body_markdown = listing_params[:body_markdown] if listing_params[:body_markdown]
@classified_listing.tag_list = listing_params[:tag_list] if listing_params[:tag_list]
@classified_listing.category = listing_params[:category] if listing_params[:category]
if listing_params[:classified_listing_category_id]
@classified_listing.classified_listing_category_id = listing_params[:classified_listing_category_id]
end
@classified_listing.location = listing_params[:location] if listing_params[:location]
@classified_listing.expires_at = listing_params[:expires_at] if listing_params[:expires_at]
@classified_listing.contact_via_connect = listing_params[:contact_via_connect] if listing_params[:contact_via_connect]
@ -45,7 +48,6 @@ module ClassifiedListingsToolkit
authorize @classified_listing, :authorized_organization_poster? if @classified_listing.organization_id.present?
@classified_listing.user_id = current_user.id
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
org = Organization.find_by(id: @classified_listing.organization_id)
if listing_params[:action] == "draft"
@ -56,6 +58,14 @@ module ClassifiedListingsToolkit
available_org_credits = org.credits.unspent if org
available_user_credits = current_user.credits.unspent
unless @classified_listing.valid?
# TODO: [thepracticaldev/oss] For now the credits are needed in the view
@credits = current_user.credits.unspent
process_unsuccessful_creation
return
end
cost = @classified_listing.cost
# we use the org's credits if available, otherwise we default to the user's
if org && available_org_credits.size >= cost
create_listing(org, cost)
@ -66,14 +76,18 @@ module ClassifiedListingsToolkit
end
end
ALLOWED_PARAMS = %i[
title body_markdown category classified_listing_category_id tag_list
expires_at contact_via_connect location organization_id action
].freeze
# Never trust parameters from the scary internet, only allow a specific list through.
def listing_params
if params["classified_listing"]["tags"].present?
params["classified_listing"]["tags"] = params["classified_listing"]["tags"].join(", ")
params["classified_listing"]["tag_list"] = params["classified_listing"].delete "tags"
end
accessible = %i[title body_markdown category tag_list expires_at contact_via_connect location organization_id action]
params.require(:classified_listing).permit(accessible)
params.require(:classified_listing).permit(ALLOWED_PARAMS)
end
def create_draft
@ -124,7 +138,7 @@ module ClassifiedListingsToolkit
def update
authorize @classified_listing
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
cost = @classified_listing.cost
# NOTE: this should probably be split in three different actions: bump, unpublish, publish
return bump_listing(cost) if listing_params[:action] == "bump"

View file

@ -3,7 +3,10 @@ class Internal::ClassifiedListingsController < Internal::ApplicationController
layout "internal"
def index
@classified_listings = ClassifiedListing.includes(%i[user organization]).page(params[:page]).order("bumped_at DESC").per(50)
@classified_listings =
ClassifiedListing.includes(%i[user classified_listing_category]).
page(params[:page]).order("bumped_at DESC").per(50)
@classified_listings = @classified_listings.where(category: params[:filter]) if params[:filter].present?
end
@ -14,25 +17,29 @@ class Internal::ClassifiedListingsController < Internal::ApplicationController
def update
@classified_listing = ClassifiedListing.find(params[:id])
handle_publish_status if listing_params[:published]
bump_listing if listing_params[:action] == "bump"
bump_listing(@classified_listing.cost) if listing_params[:action] == "bump"
update_listing_details
clear_listings_cache
flash[:success] = "Listing updated successfully"
redirect_to "/internal/listings/#{@classified_listing.id}/edit"
redirect_to edit_internal_classified_listing_path(@classified_listing)
end
def destroy
@classified_listing = ClassifiedListing.find(params[:id])
@classified_listing.destroy
flash[:warning] = "'#{@classified_listing.title}' was destroyed successfully"
redirect_to "/internal/listings"
redirect_to internal_classified_listings_path
end
private
ALLOWED_PARAMS = %i[
published body_markdown title category classified_listing_category_id tag_list action
].freeze
private_constant :ALLOWED_PARAMS
def listing_params
allowed_params = %i[published body_markdown title category tag_list action]
params.require(:classified_listing).permit(allowed_params)
params.require(:classified_listing).permit(ALLOWED_PARAMS)
end
def handle_publish_status

View file

@ -51,6 +51,7 @@ class SocialPreviewsController < ApplicationController
private
# TODO: [thepracticaldev/oss] don't hardcode this
def define_categories
cat_info = {
"collabs": ["Collaborators Wanted", "#5AE8D9"],

View file

@ -4,6 +4,7 @@ class StoriesController < ApplicationController
title path id user_id comments_count positive_reactions_count organization_id
reading_time video_thumbnail_url video video_duration_in_minutes language
experience_level_rating experience_level_rating_distribution cached_user cached_organization
classified_listing_category_id
],
methods: %i[
readable_publish_date cached_tag_list_array flare_tag class_name
@ -331,7 +332,7 @@ class StoriesController < ApplicationController
end
def assign_classified_listings
@classified_listings = ClassifiedListing.where(published: true).select(:title, :category, :slug, :bumped_at)
@classified_listings = ClassifiedListing.where(published: true).select(:title, :category, :classified_listing_category_id, :slug, :bumped_at)
end
def set_user_json_ld

View file

@ -19,7 +19,7 @@ class Categories extends Component {
details = () => {
const { categoriesForDetails } = this.props;
const rules = categoriesForDetails.map(category => {
const rules = categoriesForDetails.map((category) => {
const paragraphText = `${category.name}: ${category.rules}`;
return <p>{paragraphText}</p>;
});
@ -42,7 +42,7 @@ class Categories extends Component {
<select
id="category"
className="listingform__input"
name="classified_listing[category]"
name="classified_listing[classified_listing_category_id]"
onChange={onChange}
onBlur={onChange}
>

View file

@ -9,7 +9,7 @@ function loadElement() {
organizations,
categoriesForSelect,
categoriesForDetails,
} = root.dataset;
} = root.dataset;
render(
<ListingForm
organizations={organizations}
@ -18,7 +18,7 @@ function loadElement() {
categoriesForDetails={categoriesForDetails}
/>,
root,
root.firstElementChild
root.firstElementChild,
);
}
}

View file

@ -4,28 +4,13 @@ class ClassifiedListing < ApplicationRecord
SEARCH_SERIALIZER = Search::ClassifiedListingSerializer
SEARCH_CLASS = Search::ClassifiedListing
# TODO: remove this once we are live with the new ClassifiedListingCategory model
CATEGORIES_AVAILABLE = {
cfp: { cost: 1, name: "Conference CFP", rules: "Currently open for proposals, with link to form." },
forhire: { cost: 1, name: "Available for Hire", rules: "You are available for hire." },
collabs: { cost: 1, name: "Contributors/Collaborators Wanted", rules: "Projects looking for volunteers. Not job listings." },
education: { cost: 1, name: "Education/Courses", rules: "Educational material and/or schools/bootcamps." },
jobs: { cost: 25, name: "Job Listings", rules: "Companies offering employment right now." },
mentors: { cost: 1, name: "Offering Mentorship", rules: "You are available to mentor someone." },
products: { cost: 5, name: "Products/Tools", rules: "Must be available right now." },
mentees: { cost: 1, name: "Seeking a Mentor", rules: "You are looking for a mentor." },
forsale: { cost: 1, name: "Stuff for Sale", rules: "Personally owned physical items for sale." },
events: { cost: 1, name: "Upcoming Events", rules: "In-person or online events with date included." },
misc: { cost: 1, name: "Miscellaneous", rules: "Must not fit in any other category." }
}.with_indifferent_access.freeze
# TODO: remove this once we are live with the new ClassifiedListingCategory model
before_validation :assign_classified_listing_category
attr_accessor :action
# TODO: this is optional for now to ease the migration
belongs_to :classified_listing_category, optional: true
# This allows to create a listing from a catgory string.
# TODO: [mkohl] refactor once column was dropped.
before_validation :assign_classified_listing_category
belongs_to :classified_listing_category
belongs_to :user
belongs_to :organization, optional: true
before_save :evaluate_markdown
@ -39,10 +24,8 @@ class ClassifiedListing < ApplicationRecord
validates :user_id, presence: true
validates :organization_id, presence: true, unless: :user_id?
validates :title, presence: true,
length: { maximum: 128 }
validates :body_markdown, presence: true,
length: { maximum: 400 }
validates :title, presence: true, length: { maximum: 128 }
validates :body_markdown, presence: true, length: { maximum: 400 }
validates :location, length: { maximum: 32 }
validate :restrict_markdown_input
validate :validate_tags
@ -50,30 +33,36 @@ class ClassifiedListing < ApplicationRecord
scope :published, -> { where(published: true) }
def self.cost_by_category(category)
categories_available.dig(category, :cost) || 0
end
def author
organization || user
end
# TODO: refactor this class method block
def self.select_options_for_categories
categories_available.keys.map do |key|
category = categories_available[key]
cost = category[:cost]
["#{category[:name]} (#{cost} #{'Credit'.pluralize(cost)})", key]
ClassifiedListingCategory.select(:id, :name, :cost).map do |cl|
["#{cl.name} (#{cl.cost} #{'Credit'.pluralize(cl.cost)})", cl.id]
end
end
def self.categories_for_display
categories_available.keys.map do |key|
{ slug: key, name: categories_available[key][:name] }
ClassifiedListingCategory.pluck(:slug, :name).map do |slug, name|
{ slug: slug, name: name }
end
end
def self.categories_available
CATEGORIES_AVAILABLE
ClassifiedListingCategory.all.each_with_object({}) do |cat, h|
h[cat.slug] = cat.attributes.slice("cost", "name", "rules")
end.deep_symbolize_keys
end
def category
classified_listing_category&.slug
end
def cost
@cost = classified_listing_category&.cost ||
ClassifiedListingCategory.select(:cost).find_by(slug: category)&.cost
end
def author
organization || user
end
def path
@ -104,23 +93,26 @@ class ClassifiedListing < ApplicationRecord
errors.add(:body_markdown, "is not allowed to include liquid tags.") if markdown_string.include?("{% ")
end
def validate_category
categories = ClassifiedListingCategory.pluck(:slug)
errors.add(:category, "not a valid category") unless category.in?(categories)
end
def validate_tags
errors.add(:tag_list, "exceed the maximum of 8 tags") if tag_list.length > 8
end
def validate_category
errors.add(:category, "not a valid category") unless CATEGORIES_AVAILABLE[category]
end
def create_slug
self.slug = title.to_s.downcase.parameterize.tr("_", "") + "-" + rand(100_000).to_s(26)
self.slug = "#{title.downcase.parameterize.delete('_')}-#{rand(100_000).to_s(26)}"
end
def assign_classified_listing_category
category_name = CATEGORIES_AVAILABLE.dig(attributes["category"], :name)
category = ClassifiedListingCategory.find_by(name: category_name)
return if classified_listing_category_id.present?
category = ClassifiedListingCategory.find_by(slug: attributes["category"])
return unless category
self.category = category.slug
self.classified_listing_category_id = category.id
end
end

View file

@ -1,10 +1,16 @@
json.type_of "classified_listing"
json.id listing.id
json.title listing.title
json.slug listing.slug
json.body_markdown listing.body_markdown
json.tag_list listing.cached_tag_list
json.tags listing.tag_list
json.category listing.category
json.processed_html listing.processed_html
json.published listing.published
json.type_of "classified_listing"
json.extract!(
listing,
:id,
:title,
:slug,
:body_markdown,
:category,
:classified_listing_category_id,
:processed_html,
:published,
)
json.tag_list listing.cached_tag_list
json.tags listing.tag_list

View file

@ -2,7 +2,7 @@
<h2>Edit <%= link_to @classified_listing.title, "/listings/#{@classified_listing.category}/#{@classified_listing.slug}", target: "_blank", rel: "noopener" %></h2>
<%= form_for [:internal, @classified_listing] do |form| %>
<%= form_with model: [:internal, @classified_listing], method: :patch do |form| %>
<div class="form-group">
<%= form.label :title %>
<%= form.text_field :title, size: 100, maxlength: 128, class: "form-control" %>
@ -17,8 +17,8 @@
<%= form.text_field :tag_list, value: @classified_listing.cached_tag_list, size: 100, class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :category %>
<%= form.select :category, options_for_select(ClassifiedListing.select_options_for_categories.map(&:last), @classified_listing.category) %>
<%= form.label :classified_listing_category_id %>
<%= form.select :classified_listing_category_id, ClassifiedListing.select_options_for_categories %>
</div>
<div class="form-check">
<%= form.check_box :published, checked: @classified_listing.published? %>
@ -28,7 +28,7 @@
<% end %>
<hr class="mt-5">
<div class="d-flex justify-content-start">
<%= form_for [:internal, @classified_listing] do |f| %>
<%= form_with model: [:internal, @classified_listing], method: :patch do |f| %>
<input type="hidden" name="classified_listing[action]" value="bump" />
<%= f.submit "Bump Listing ⏫", class: "btn btn-secondary" %>
<% end %>

View file

@ -47,11 +47,12 @@ Rails.application.routes.draw do
resources :articles, only: %i[index show update]
resources :broadcasts, only: %i[index new create edit update]
resources :buffer_updates, only: %i[create update]
# TODO: [mkohl] Change this to a single resource definition
resources :classified_listings, only: %i[index edit update destroy]
resources :listings, only: %i[index edit update destroy], controller: "classified_listings"
resources :comments, only: [:index]
resources :events, only: %i[index create update]
resources :feedback_messages, only: %i[index show]
resources :listings, only: %i[index edit update destroy], controller: "classified_listings"
resources :pages, only: %i[index new create edit update destroy]
resources :mods, only: %i[index update]
resources :moderator_actions, only: %i[index]

View file

@ -418,7 +418,54 @@ end
##############################################################################
puts <<-ASCII # rubocop:disable Rails/Output
counter += 1
Rails.logger.info "#{counter}. Creating Classified Listing Categories"
CATEGORIES = [
{
slug: "cfp",
cost: 1,
name: "Conference CFP",
rules: "Currently open for proposals,with link to form."
},
{
slug: "education",
cost: 1,
name: "Education/Courses",
rules: "Educational material and/or schools/bootcamps."
},
{
slug: "jobs",
cost: 25,
name: "Job Listings",
rules: "Companies offering employment right now."
},
{
slug: "forsale",
cost: 1,
name: "Stuff for Sale",
rules: "Personally owned physical items for sale."
},
{
slug: "events",
cost: 1,
name: "Upcoming Events",
rules: "In-person or online events with date included."
},
{
slug: "misc",
cost: 1,
name: "Miscellaneous",
rules: "Must not fit in any other category."
}
].freeze
CATEGORIES.each { |attributes| ClassifiedListingCategory.create(attributes) }
##############################################################################
# rubocop:disable Rails/Output
puts <<-ASCII
@ -447,3 +494,4 @@ puts <<-ASCII # rubocop:disable Rails/Output
All done!
ASCII
# rubocop:enable Rails/Output

View file

@ -7,6 +7,8 @@ FactoryBot.define do
trait :cfp do
name { "Conference CFP" }
slug { "cfp" }
cost { 5 }
end
end
end

View file

@ -3,8 +3,14 @@ FactoryBot.define do
user
title { Faker::Book.title + " #{rand(1000)}" }
body_markdown { Faker::Hipster.paragraph(sentence_count: 2) }
category { "education" }
published { true }
bumped_at { Time.current }
after(:build) do |cl|
if cl.classified_listing_category_id.blank?
category = ClassifiedListingCategory.first || create(:classified_listing_category)
cl.classified_listing_category_id = category.id
end
end
end
end

View file

@ -32,18 +32,6 @@ RSpec.describe ClassifiedListing, type: :model do
end
end
# TODO: remove this once we are live with the new ClassifiedListingCategory model
describe "classified listing category" do
it "automatically assigns a category on save" do
create(:classified_listing_category)
cl = build(:classified_listing, user_id: user.id)
cl.save
expect(cl).to be_valid
expect(cl.reload.classified_listing_category).to be_present
end
end
describe "body html" do
it "converts markdown to html" do
expect(classified_listing.processed_html).to include("<p>")
@ -100,15 +88,5 @@ RSpec.describe ClassifiedListing, type: :model do
end
end
end
describe ".cost_by_category" do
it "returns the cost per category" do
expected_cost = described_class::CATEGORIES_AVAILABLE.dig("cfp", "cost")
expect(described_class.cost_by_category("cfp")).to eq(expected_cost)
end
it "returns 0 with invalid category" do
expect(described_class.cost_by_category("invalid")).to eq(0)
end
end
end

View file

@ -1,6 +1,13 @@
require "rails_helper"
RSpec.describe "Api::V0::ClassifiedListings", type: :request do
let_it_be_readonly(:cfp_category) do
create(:classified_listing_category, :cfp)
end
let_it_be_readonly(:edu_category) do
create(:classified_listing_category)
end
shared_context "when user is authorized" do
let(:api_secret) { create(:api_secret) }
let(:user) { api_secret.user }
@ -12,14 +19,14 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
{
title: "Title",
body_markdown: "Markdown text",
category: "cfp"
classified_listing_category_id: cfp_category.id
}
end
let(:draft_params) do
{
title: "Title draft",
body_markdown: "Markdown draft text",
category: "cfp",
classified_listing_category_id: cfp_category.id,
action: "draft"
}
end
@ -36,8 +43,8 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
let(:user2) { create(:user) }
before do
create_list(:classified_listing, 3, user: user1, category: "cfp")
create_list(:classified_listing, 4, user: user2)
create_list(:classified_listing, 3, user: user1, classified_listing_category_id: cfp_category.id)
create_list(:classified_listing, 4, user: user2, classified_listing_category_id: edu_category.id)
end
end
@ -239,21 +246,31 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
describe "user must have enough credit to create a classified listing" do
include_context "when user is authorized"
include_context "when param list is valid"
it "fails to create a classified listing if user does not have enough credit" do
post_classified_listing(category: "cfp")
post_classified_listing(listing_params)
expect(response).to have_http_status(:payment_required)
end
it "fails to create a classifiedlisting if the org does not have enough credit" do
org = user_admin_organization(user)
post_classified_listing(category: "cfp", organization_id: org.id)
post_classified_listing(
**listing_params,
organization_id: org.id,
)
expect(response).to have_http_status(:payment_required)
end
end
describe "user cannot create a classified with a request lacking mandatory parameters" do
let(:invalid_params) { { title: "Title", category: "cfp" } }
let(:invalid_params) do
{
title: "Title",
category: "cfp",
classified_listing_category_id: cfp_category.id
}
end
include_context "when user is authorized"
include_context "when user has enough credit"
@ -338,15 +355,9 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "cannot create a draft due to internal error" do
listing = create(:classified_listing, user_id: user.id)
allow(ClassifiedListing).to receive_messages(cost_by_category: nil, new: listing)
allow(Organization).to receive(:find_by)
allow(listing).to receive(:save) do
listing.errors.add(:base)
false
end
post_classified_listing(draft_params)
expect(response.parsed_body["errors"]["base"]).to eq(["is invalid"])
post_classified_listing(draft_params.except(:classified_listing_category_id))
expect(response.parsed_body["errors"]["category"]).to eq(["not a valid category"])
expect(response).to have_http_status(:unprocessable_entity)
end
@ -384,7 +395,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
expect(listing.title).to eq(listing_params[:title])
expect(listing.body_markdown).to eq(listing_params[:body_markdown])
expect(listing.category).to eq(listing_params[:category])
expect(listing.category).to eq(cfp_category.slug)
end
it "creates a classified listing with a location" do
@ -491,7 +502,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "bumps the listing and subtract credits" do
cost = ClassifiedListing.cost_by_category(listing.category)
cost = listing.cost
create_list(:credit, cost, user: user)
previous_bumped_at = listing.bumped_at
expect do
@ -501,7 +512,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "bumps the org listing using org credits before user credits" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
cost = org_listing.cost
create_list(:credit, cost, organization: organization)
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
@ -512,7 +523,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "bumps the org listing using user credits if org credits insufficient and user credits are" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
cost = org_listing.cost
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
expect do
@ -526,7 +537,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
include_context "when user is authorized"
it "publishes a draft and charges user credits if first publish" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
cost = listing_draft.cost
create_list(:credit, cost, user: user)
expect do
put_classified_listing(listing_draft.id, action: "publish")
@ -534,14 +545,14 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "publishes a draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
cost = listing_draft.cost
create_list(:credit, cost, user: user)
put_classified_listing(listing_draft.id, action: "publish")
expect(listing_draft.reload.published).to eq(true)
end
it "publishes an org draft and charges org credits if first publish" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
cost = org_listing_draft.cost
create_list(:credit, cost, organization: organization)
expect do
put_classified_listing(org_listing_draft.id, action: "publish")
@ -549,7 +560,7 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "publishes an org draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
cost = org_listing_draft.cost
create_list(:credit, cost, organization: organization)
put_classified_listing(org_listing_draft.id, action: "publish")
expect(org_listing_draft.reload.published).to eq(true)
@ -594,7 +605,8 @@ RSpec.describe "Api::V0::ClassifiedListings", type: :request do
end
it "fails if category is invalid" do
put_classified_listing(listing.id, title: "New title", category: "unknown")
max_id = ClassifiedListingCategory.maximum(:id)
put_classified_listing(listing.id, title: "New title", classified_listing_category_id: max_id + 1)
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body.dig("errors", "category").first).to match(/not a valid category/)
end

View file

@ -1,13 +1,16 @@
require "rails_helper"
RSpec.describe "ClassifiedListings", type: :request do
let_it_be_readonly(:edu_category) do
create(:classified_listing_category, cost: 1)
end
let(:user) { create(:user) }
let(:listing_params) do
{
classified_listing: {
title: "something",
body_markdown: "something else",
category: "cfp",
classified_listing_category_id: edu_category.id,
tag_list: "",
contact_via_connect: true
}
@ -18,7 +21,7 @@ RSpec.describe "ClassifiedListings", type: :request do
classified_listing: {
title: "this a draft",
body_markdown: "something draft",
category: "cfp",
classified_listing_category_id: edu_category.id,
tag_list: "",
contact_via_connect: true,
action: "draft"
@ -144,13 +147,15 @@ RSpec.describe "ClassifiedListings", type: :request do
create_list(:credit, 25, user: user)
end
let_it_be_readonly(:cfp_category) { create(:classified_listing_category, :cfp) }
context "when the listing is invalid" do
let(:invalid_params) do
{
classified_listing: {
title: "nothing",
body_markdown: "",
category: "cfp",
classified_listing_category_id: cfp_category.id,
tag_list: ""
}
}
@ -195,9 +200,9 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "properly deducts the amount of credits" do
post "/listings", params: listing_params
listing_cost = ClassifiedListing.categories_available[:cfp][:cost]
expect(user.credits.spent.size).to eq(listing_cost)
expect do
post "/listings", params: listing_params
end.to change { user.credits.spent.size }.by(edu_category.cost)
end
it "creates a listing draft under the org" do
@ -305,7 +310,7 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "bumps the listing and subtract credits" do
cost = ClassifiedListing.cost_by_category(listing.category)
cost = listing.cost
create_list(:credit, cost, user: user)
previous_bumped_at = listing.bumped_at
expect do
@ -315,7 +320,7 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "bumps the org listing using org credits before user credits" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
cost = org_listing.cost
create_list(:credit, cost, organization: organization)
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
@ -326,7 +331,7 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "bumps the org listing using user credits if org credits insufficient and user credits are" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
cost = org_listing.cost
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
expect do
@ -340,7 +345,7 @@ RSpec.describe "ClassifiedListings", type: :request do
let(:params) { { classified_listing: { action: "publish" } } }
it "publishes a draft and charges user credits if first publish" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
cost = listing_draft.cost
create_list(:credit, cost, user: user)
expect do
put "/listings/#{listing_draft.id}", params: params
@ -348,14 +353,14 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "publishes a draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
cost = listing_draft.cost
create_list(:credit, cost, user: user)
put "/listings/#{listing_draft.id}", params: params
expect(listing_draft.reload.published).to eq(true)
end
it "publishes an org draft and charges org credits if first publish" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
cost = org_listing_draft.cost
create_list(:credit, cost, organization: organization)
expect do
put "/listings/#{org_listing_draft.id}", params: params
@ -363,7 +368,7 @@ RSpec.describe "ClassifiedListings", type: :request do
end
it "publishes an org draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
cost = org_listing_draft.cost
create_list(:credit, cost, organization: organization)
put "/listings/#{org_listing_draft.id}", params: params
expect(org_listing_draft.reload.published).to eq(true)

View file

@ -3,8 +3,20 @@ require "rails_helper"
RSpec.describe "/listings", type: :request do
let(:user) { create(:user) }
let(:organization) { create(:organization) }
let(:cl_category) { create(:classified_listing_category, cost: 1) }
describe "GETS /listings" do
let(:params) do
{
classified_listing: {
title: "Hey",
classified_listing_category_id: cl_category.id,
body_markdown: "hey hey my my",
tag_list: "ruby, rails, go"
}
}
end
describe "GET /listings" do
it "has page content" do
get "/listings"
expect(response.body).to include("classified-filters")
@ -30,63 +42,51 @@ RSpec.describe "/listings", type: :request do
end
it "creates proper listing if credits are available" do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my" }
}
post "/listings", params: params
expect(ClassifiedListing.last.processed_html).to include("hey my")
end
it "spends credits" do
num_credits = Credit.where(spent: true).size
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my" }
}
post "/listings", params: params
expect(Credit.where(spent: true).size).to be > num_credits
end
it "adds tags" do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
post "/listings", params: params
expect(ClassifiedListing.last.cached_tag_list).to include("rails")
end
it "creates the listing under the user" do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
post "/listings", params: params
expect(ClassifiedListing.last.user_id).to eq user.id
end
it "creates the listing for the user if no organization_id is selected" do
create(:organization_membership, user_id: user.id, organization_id: organization.id)
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
post "/listings", params: params
expect(ClassifiedListing.last.organization_id).to eq nil
expect(ClassifiedListing.last.user_id).to eq user.id
end
it "creates the listing for the organization" do
create(:organization_membership, user_id: user.id, organization_id: organization.id)
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go", organization_id: organization.id }
}
params[:classified_listing][:organization_id] = organization.id
post "/listings", params: params
expect(ClassifiedListing.last.organization_id).to eq(organization.id)
end
it "does not create an org listing if a different org member doesn't belong to the org" do
another_org = create(:organization)
create(:organization_membership, user_id: user.id, organization_id: another_org.id)
params[:classified_listing][:organization_id] = organization.id
expect do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go", organization_id: organization.id }
}
post "/listings", params: params
end.to raise_error Pundit::NotAuthorizedError
end
end
describe "GETS /listings/edit" do
describe "GET /listings/edit" do
let(:classified_listing) { create(:classified_listing, user_id: user.id) }
before do

View file

@ -7,7 +7,6 @@ RSpec.describe "SocialPreviews", type: :request do
let(:article) { create(:article, user_id: user.id, tags: tag.name) }
let(:comment) { create(:comment, user_id: user.id, commentable: article) }
let(:image_url) { "https://hcti.io/v1/image/6c52de9d-4d37-4008-80f8-67155589e1a1" }
let(:listing) { create(:classified_listing, user_id: user.id, category: "cfp") }
before do
stub_request(:post, /hcti.io/).
@ -136,9 +135,11 @@ RSpec.describe "SocialPreviews", type: :request do
end
describe "GET /social_previews/listing/:id" do
let(:listing) { create(:classified_listing, user_id: user.id) }
it "renders pretty category name" do
get "/social_previews/listing/#{listing.id}"
expect(response.body).to include CGI.escapeHTML("Call For Proposal")
expect(response.body).to include CGI.escapeHTML("Education")
end
it "renders consistent HTML between requests" do

View file

@ -31,9 +31,10 @@ RSpec.describe Search::ClassifiedListing, type: :service, elasticsearch: true do
context "with a term filter" do
it "searches by category" do
classified_listing.update(category: "forhire")
new_category = create(:classified_listing_category, :cfp)
classified_listing.update(classified_listing_category_id: new_category.id)
index_documents(classified_listing)
params = { size: 5, category: "forhire" }
params = { size: 5, category: new_category.slug }
classified_listing_docs = described_class.search_documents(params: params)
expect(classified_listing_docs.count).to eq(1)
@ -121,8 +122,9 @@ RSpec.describe Search::ClassifiedListing, type: :service, elasticsearch: true do
end
it "sorts documents for a given field" do
classified_listing.update(category: "forhire")
classified_listing2 = FactoryBot.create(:classified_listing, category: "cfp")
classified_listing = create(:classified_listing)
cfp = create(:classified_listing_category, :cfp)
classified_listing2 = create(:classified_listing, classified_listing_category_id: cfp.id)
index_documents([classified_listing, classified_listing2])
params = { size: 5, sort_by: "category", sort_direction: "asc" }