Add ability for users to publish series (#1034)
This commit is contained in:
parent
b6ac7a8b64
commit
359cc71b64
17 changed files with 181 additions and 21 deletions
|
|
@ -247,13 +247,13 @@ header{
|
|||
.article-collection-wrapper{
|
||||
text-align:center;
|
||||
font-family: $helvetica;
|
||||
font-size:15px;
|
||||
font-size:16px;
|
||||
p{
|
||||
border-bottom:2px solid $black;
|
||||
width:120px;
|
||||
max-width:90%;
|
||||
margin:35px auto 15px;
|
||||
padding:2px 0px;
|
||||
font-weight:bold;
|
||||
font-style: oblique;
|
||||
}
|
||||
.article-collection{
|
||||
display:inline-block;
|
||||
|
|
@ -265,19 +265,19 @@ header{
|
|||
margin-bottom: 1.1vw;
|
||||
a{
|
||||
color:$black;
|
||||
padding:calc(0.8vw + 10px);
|
||||
background:$lightest-gray;
|
||||
padding:calc(0.3vw + 14px);
|
||||
background:darken($lightest-gray, 12%);
|
||||
display:inline-block;
|
||||
position:relative;
|
||||
z-index:4;
|
||||
max-width:260px;
|
||||
margin:0.2vw;
|
||||
border-radius:5px;
|
||||
border-radius:100px;
|
||||
&:first-child{
|
||||
// margin-left:-5px;
|
||||
}
|
||||
&:hover{
|
||||
background:darken($lightest-gray,3%);
|
||||
background:darken($lightest-gray,18%);
|
||||
}
|
||||
&.current-article{
|
||||
background: $black;
|
||||
|
|
|
|||
|
|
@ -371,6 +371,10 @@
|
|||
@media screen and ( min-width: 600px ){
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
button {
|
||||
margin-left: 5px;
|
||||
background: $bold-blue;
|
||||
}
|
||||
}
|
||||
.articleform__exitbutton{
|
||||
position:absolute;
|
||||
|
|
@ -392,6 +396,15 @@
|
|||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.articleform__donebutton{
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
background: $green;
|
||||
color: $black;
|
||||
padding: 10px 40px;
|
||||
margin: 20px auto;
|
||||
|
||||
}
|
||||
img{
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
|
|
|
|||
|
|
@ -68,9 +68,12 @@ module Api
|
|||
else
|
||||
params["article"]["organization_id"] = nil
|
||||
end
|
||||
if params["article"]["series"].present?
|
||||
params["article"]["collection_id"] = Collection.find_series(params["article"]["series"], current_user)&.id
|
||||
end
|
||||
params.require(:article).permit(
|
||||
:title, :body_markdown, :user_id, :main_image, :published, :description,
|
||||
:tag_list, :organization_id, :canonical_url
|
||||
:tag_list, :organization_id, :canonical_url, :series, :collection_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -45,19 +45,19 @@ class ArticlesController < ApplicationController
|
|||
@article = if @tag.present? && @user&.editor_version == "v2"
|
||||
authorize Article
|
||||
Article.new(body_markdown: "", cached_tag_list: @tag.name,
|
||||
processed_html: "")
|
||||
processed_html: "", user_id: current_user&.id)
|
||||
elsif @tag&.submission_template.present? && @user
|
||||
authorize Article
|
||||
Article.new(body_markdown: @tag.submission_template_customized(@user.name),
|
||||
processed_html: "")
|
||||
processed_html: "", user_id: current_user&.id)
|
||||
else
|
||||
skip_authorization
|
||||
if @user&.editor_version == "v2"
|
||||
Article.new
|
||||
Article.new(user_id: current_user&.id)
|
||||
else
|
||||
Article.new(
|
||||
body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n---\n\n",
|
||||
processed_html: "",
|
||||
processed_html: "", user_id: current_user&.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ export default class ArticleForm extends Component {
|
|||
tagList: article.cached_tag_list || '',
|
||||
description: '',
|
||||
canonicalUrl: article.canonical_url || '',
|
||||
series: article.series || '',
|
||||
allSeries: article.all_series || [],
|
||||
bodyMarkdown: article.body_markdown || '',
|
||||
published: article.published || false,
|
||||
previewShowing: false,
|
||||
|
|
@ -118,6 +120,7 @@ export default class ArticleForm extends Component {
|
|||
};
|
||||
|
||||
handleConfigChange = e => {
|
||||
e.preventDefault();
|
||||
let newState = {}
|
||||
newState[e.target.name] = e.target.value;
|
||||
this.setState(newState)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export default class ImageManagement extends Component {
|
|||
{mainImageArea}
|
||||
<h2>Body Images</h2>
|
||||
{inertionImageArea}
|
||||
<div><button class="articleform__donebutton" onClick={onExit}>Done</button></div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,21 @@ export default class MoreConfig extends Component {
|
|||
// this.state = {insertionImageUrl: null}
|
||||
}
|
||||
|
||||
handleSeriesButtonClick = e => {
|
||||
e.preventDefault();
|
||||
this.props.onConfigChange(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onExit, passedData, onSaveDraft } = this.props;
|
||||
let publishedField = '';
|
||||
let seriesTip = <small>Will this post be part of a series? Give the series a unique name. (Series visible once it has multiple posts)</small>
|
||||
if (passedData.allSeries.length > 0) {
|
||||
const seriesNames = passedData.allSeries.map( name => {
|
||||
return <button name='series' onClick={this.props.onConfigChange} value={name}>{name}</button>
|
||||
})
|
||||
seriesTip = <small>Existing series: {seriesNames}</small>
|
||||
}
|
||||
if (passedData.published) {
|
||||
publishedField = <div>
|
||||
<h4>Danger Zone</h4>
|
||||
|
|
@ -35,6 +47,12 @@ export default class MoreConfig extends Component {
|
|||
<input type="text" value={passedData.canonicalUrl} name="canonicalUrl" onKeyUp={this.props.onConfigChange}/>
|
||||
</div>
|
||||
<small>Change meta tag <code>canonical_url</code> if this post was first published elsewhere (like your own blog)</small>
|
||||
<div>
|
||||
<label>Series Name</label>
|
||||
<input type="text" value={passedData.series} name="series" onKeyUp={this.props.onConfigChange}/>
|
||||
</div>
|
||||
{seriesTip}
|
||||
<div><button class="articleform__donebutton" onClick={onExit}>Done</button></div>
|
||||
{publishedField}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class Article < ApplicationRecord
|
|||
|
||||
acts_as_taggable_on :tags
|
||||
|
||||
attr_accessor :publish_under_org
|
||||
attr_accessor :publish_under_org, :series
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :job_opportunity, optional: true
|
||||
|
|
@ -31,6 +31,7 @@ class Article < ApplicationRecord
|
|||
validates :body_markdown, uniqueness: { scope: %i[user_id title] }
|
||||
validate :validate_tag
|
||||
validate :validate_video
|
||||
validate :validate_collection_permission
|
||||
validates :video_state, inclusion: { in: %w(PROGRESSING COMPLETED) }, allow_nil: true
|
||||
validates :cached_tag_list, length: { maximum: 86 }
|
||||
validates :main_image, url: { allow_blank: true, schemes: ["https", "http"] }
|
||||
|
|
@ -359,6 +360,16 @@ class Article < ApplicationRecord
|
|||
end
|
||||
handle_asynchronously :async_score_calc
|
||||
|
||||
def series
|
||||
# name of series article is part of
|
||||
collection&.slug
|
||||
end
|
||||
|
||||
def all_series
|
||||
# all series names
|
||||
user&.collections&.pluck(:slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# def send_to_moderator
|
||||
|
|
@ -387,6 +398,7 @@ class Article < ApplicationRecord
|
|||
self.main_image = front_matter["cover_image"] if front_matter["cover_image"].present?
|
||||
self.canonical_url = front_matter["canonical_url"] if front_matter["canonical_url"].present?
|
||||
self.description = front_matter["description"] || token_msg
|
||||
self.collection_id = Collection.find_series(front_matter["series"], user).id if front_matter["series"].present?
|
||||
if front_matter["automatically_renew"].present? && tag_list.include?("hiring")
|
||||
self.automatically_renew = front_matter["automatically_renew"]
|
||||
end
|
||||
|
|
@ -418,6 +430,12 @@ class Article < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def validate_collection_permission
|
||||
if collection && collection.user_id != user_id
|
||||
errors.add(:collection_id, "must be one you have permission to post to")
|
||||
end
|
||||
end
|
||||
|
||||
def create_slug
|
||||
if slug.blank? && title.present? && !published
|
||||
self.slug = title_to_slug + "-temp-slug-#{rand(10_000_000)}"
|
||||
|
|
|
|||
|
|
@ -2,4 +2,12 @@ class Collection < ApplicationRecord
|
|||
has_many :articles
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :organization, optional: true
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :slug, uniqueness: { scope: :user_id }
|
||||
|
||||
def self.find_series(slug, user)
|
||||
series = Collection.where(slug: slug, user: user).first
|
||||
series || Collection.create(slug: slug, user: user)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class ArticleCreationService
|
|||
if user.organization_id.present? && article_params[:publish_under_org].to_i == 1
|
||||
article.organization_id = user.organization_id
|
||||
end
|
||||
article.collection_id = Collection.find_series(article_params[:series], user).id if article_params[:series].present?
|
||||
create_job_opportunity(article)
|
||||
if article.save
|
||||
if article.published
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
|
||||
|
||||
<% if @article.collection.present? && @article.collection.articles.size > 1 %>
|
||||
<% @collection = @article.collection %>
|
||||
<% if @collection.present? && @collection.articles.where(published: true).size > 1 %>
|
||||
<div class="article-collection-wrapper article-collection-wrapper-<%= position %>">
|
||||
<p>Part of a series</p>
|
||||
<% if @collection.slug.present? %>
|
||||
<p>Part of "<%= @collection.slug %>" series</p>
|
||||
<% else %>
|
||||
<p>Part of a series</p>
|
||||
<% end %>
|
||||
<div class="article-collection">
|
||||
<% @article.collection.articles.order("collection_position ASC").each_with_index do |article,i| %>
|
||||
<% @collection.articles.where(published: true).order("published_at ASC").each_with_index do |article,i| %>
|
||||
<a
|
||||
href="<%= article.path if article.published %>"
|
||||
class="<%= collection_link_class(@article,article) %>"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="articleform"
|
||||
id="article-form"
|
||||
data-article="<%= @article.
|
||||
to_json(only: [:id, :title, :cached_tag_list, :published, :body_markdown, :main_image, :organization_id, :canonical_url]) %>"
|
||||
to_json(only: [:id, :title, :cached_tag_list, :published, :body_markdown, :main_image, :organization_id, :canonical_url], methods: [:series, :all_series]) %>"
|
||||
data-organization="<%= @organization&.to_json(only: [:name, :bg_color_hex, :text_color_hex], methods: [:profile_image_90]) %>"
|
||||
>
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,9 @@
|
|||
<div class="body" data-article-id="<%= @article.id %>" id="article-body" itemprop="articleBody">
|
||||
<%= @article.processed_html.html_safe %>
|
||||
</div>
|
||||
<%= render "articles/collection", position: "bottom" %>
|
||||
<% if @article.body_markdown && @article.body_markdown.size > 900 %>
|
||||
<%= render "articles/collection", position: "bottom" %>
|
||||
<% end %>
|
||||
<%= render 'articles/actions' %>
|
||||
</section>
|
||||
<% if @article.body_markdown && @article.body_markdown.size > 900 %>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ task remove_old_html_variant_data: :environment do
|
|||
HtmlVariantTrial.where("created_at < ?", 1.week.ago).destroy_all
|
||||
HtmlVariantSuccess.where("created_at < ?", 1.week.ago).destroy_all
|
||||
HtmlVariant.find_each do |html_variant|
|
||||
return if html_variant.html_variant_successes.size < 12
|
||||
if html_variant.html_variant_successes.size > 3
|
||||
html_variant.calculate_success_rate!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -473,5 +473,17 @@ RSpec.describe Article, type: :model do
|
|||
last_year = 1.year.ago.year % 100
|
||||
expect(article.readable_publish_date.include?("'#{last_year}")).to eq(true)
|
||||
end
|
||||
|
||||
it "is valid as part of a collection" do
|
||||
collection = Collection.create(user_id: article.user.id, slug: "yoyoyo")
|
||||
article.collection_id = collection.id
|
||||
expect(article).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid as part of a collection that does not belong to user" do
|
||||
collection = Collection.create(user_id: 32443, slug: "yoyoyo")
|
||||
article.collection_id = collection.id
|
||||
expect(article).not_to be_valid
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations
|
||||
|
|
|
|||
|
|
@ -67,4 +67,70 @@ RSpec.describe "ArticlesApi", type: :request do
|
|||
expect(JSON.parse(response.body)["title"]).to eq(article.title)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/articles w/ current_user" do
|
||||
before do
|
||||
sign_in user1
|
||||
end
|
||||
it "creates ordinary article with proper params" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
post "/api/articles", params: {
|
||||
article: { title: new_title, body_markdown: "Yo ho ho#{rand(100)}", tag_list: "yo" }
|
||||
}
|
||||
expect(Article.last.user_id).to eq(user1.id)
|
||||
end
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
it "creates article with front matter params" do
|
||||
post "/api/articles", params: {
|
||||
article: {
|
||||
body_markdown: "---\ntitle: hey hey hahuu\npublished: false\n---\nYo ho ho#{rand(100)}",
|
||||
tag_list: "yo"
|
||||
}
|
||||
}
|
||||
expect(Article.last.title).to eq("hey hey hahuu")
|
||||
end
|
||||
it "creates article w/ series param" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
post "/api/articles", params: {
|
||||
article: { title: new_title,
|
||||
body_markdown: "Yo ho ho#{rand(100)}",
|
||||
tag_list: "yo",
|
||||
series: "helloyo",
|
||||
}
|
||||
}
|
||||
expect(Article.last.collection).to eq(Collection.find_by_slug("helloyo"))
|
||||
expect(Article.last.collection.user_id).to eq(Article.last.user_id)
|
||||
end
|
||||
it "creates article with front matter params" do
|
||||
post "/api/articles", params: {
|
||||
article: {
|
||||
body_markdown: "---\ntitle: hey hey hahuu\npublished: false\nseries: helloyo\n---\nYo ho ho#{rand(100)}",
|
||||
tag_list: "yo"
|
||||
}
|
||||
}
|
||||
expect(Article.last.collection).to eq(Collection.find_by_slug("helloyo"))
|
||||
expect(Article.last.collection.user_id).to eq(Article.last.user_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/articles/:id w/ current_user" do
|
||||
before do
|
||||
sign_in user1
|
||||
@article = create(:article, user: user1)
|
||||
end
|
||||
it "updates ordinary article with proper params" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
put "/api/articles/#{@article.id}", params: {
|
||||
article: { title: new_title, body_markdown: "Yo ho ho#{rand(100)}", tag_list: "yo" }
|
||||
}
|
||||
expect(Article.last.title).to eq(new_title)
|
||||
end
|
||||
it "updates ordinary article with proper params" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
put "/api/articles/#{@article.id}", params: {
|
||||
article: { title: new_title, body_markdown: "Yo ho ho#{rand(100)}", tag_list: "yo" }
|
||||
}
|
||||
expect(Article.last.title).to eq(new_title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,5 +50,16 @@ RSpec.describe "ArticlesCreate", type: :request do
|
|||
}
|
||||
expect(Article.last.job_opportunity.remoteness).to eq("on_premise")
|
||||
end
|
||||
|
||||
it "creates series when series is created with frontmatter" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
post "/articles", params: {
|
||||
article: {
|
||||
title: new_title,
|
||||
body_markdown: "---\ntitle: hey hey hahuu\npublished: false\nseries: helloyo\n---\nYo ho ho#{rand(100)}",
|
||||
}
|
||||
}
|
||||
expect(Collection.last.slug).to eq("helloyo")
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue