From 359cc71b6472705975097240fc12e2a6afc81cf5 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Mon, 29 Oct 2018 13:04:42 -0400 Subject: [PATCH] Add ability for users to publish series (#1034) --- app/assets/stylesheets/article-show.scss | 14 ++-- .../stylesheets/preact/article-form.scss | 13 ++++ app/controllers/api/v0/articles_controller.rb | 5 +- app/controllers/articles_controller.rb | 8 +-- app/javascript/article-form/articleForm.jsx | 3 + .../article-form/elements/imageManagement.jsx | 1 + .../article-form/elements/moreConfig.jsx | 18 +++++ app/models/article.rb | 20 +++++- app/models/collection.rb | 8 +++ app/services/article_creation_service.rb | 1 + app/views/articles/_collection.html.erb | 13 ++-- app/views/articles/_v2_form.html.erb | 2 +- app/views/articles/show.html.erb | 4 +- lib/tasks/fetch.rake | 3 +- spec/models/article_spec.rb | 12 ++++ spec/requests/articles_api_spec.rb | 66 +++++++++++++++++++ spec/requests/articles_create_spec.rb | 11 ++++ 17 files changed, 181 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/article-show.scss b/app/assets/stylesheets/article-show.scss index b5f20a03b..d1f33fa9b 100644 --- a/app/assets/stylesheets/article-show.scss +++ b/app/assets/stylesheets/article-show.scss @@ -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; diff --git a/app/assets/stylesheets/preact/article-form.scss b/app/assets/stylesheets/preact/article-form.scss index 049416338..f7b3e6cc9 100644 --- a/app/assets/stylesheets/preact/article-form.scss +++ b/app/assets/stylesheets/preact/article-form.scss @@ -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; diff --git a/app/controllers/api/v0/articles_controller.rb b/app/controllers/api/v0/articles_controller.rb index 804c5a7e1..352fb0b78 100644 --- a/app/controllers/api/v0/articles_controller.rb +++ b/app/controllers/api/v0/articles_controller.rb @@ -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 diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 6728259c8..139a3340d 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -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 diff --git a/app/javascript/article-form/articleForm.jsx b/app/javascript/article-form/articleForm.jsx index ab677cc93..bad332bf5 100644 --- a/app/javascript/article-form/articleForm.jsx +++ b/app/javascript/article-form/articleForm.jsx @@ -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) diff --git a/app/javascript/article-form/elements/imageManagement.jsx b/app/javascript/article-form/elements/imageManagement.jsx index 8c4ee28ce..116b8a342 100644 --- a/app/javascript/article-form/elements/imageManagement.jsx +++ b/app/javascript/article-form/elements/imageManagement.jsx @@ -72,6 +72,7 @@ export default class ImageManagement extends Component { {mainImageArea}

Body Images

{inertionImageArea} +
} diff --git a/app/javascript/article-form/elements/moreConfig.jsx b/app/javascript/article-form/elements/moreConfig.jsx index b47385d41..1c321e24c 100644 --- a/app/javascript/article-form/elements/moreConfig.jsx +++ b/app/javascript/article-form/elements/moreConfig.jsx @@ -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 = Will this post be part of a series? Give the series a unique name. (Series visible once it has multiple posts) + if (passedData.allSeries.length > 0) { + const seriesNames = passedData.allSeries.map( name => { + return + }) + seriesTip = Existing series: {seriesNames} + } if (passedData.published) { publishedField =

Danger Zone

@@ -35,6 +47,12 @@ export default class MoreConfig extends Component {
Change meta tag canonical_url if this post was first published elsewhere (like your own blog) +
+ + +
+ {seriesTip} +
{publishedField} } diff --git a/app/models/article.rb b/app/models/article.rb index 2002e8f03..bd414c445 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -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)}" diff --git a/app/models/collection.rb b/app/models/collection.rb index 34b5b5f80..6751fa471 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -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 diff --git a/app/services/article_creation_service.rb b/app/services/article_creation_service.rb index ec0d3ed0e..7010c32f8 100644 --- a/app/services/article_creation_service.rb +++ b/app/services/article_creation_service.rb @@ -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 diff --git a/app/views/articles/_collection.html.erb b/app/views/articles/_collection.html.erb index 6488fd19b..0f2c02cb1 100644 --- a/app/views/articles/_collection.html.erb +++ b/app/views/articles/_collection.html.erb @@ -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 %>
-

Part of a series

+ <% if @collection.slug.present? %> +

Part of "<%= @collection.slug %>" series

+ <% else %> +

Part of a series

+ <% end %>
- <% @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| %> " data-organization="<%= @organization&.to_json(only: [:name, :bg_color_hex, :text_color_hex], methods: [:profile_image_90]) %>" > diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb index fe17df223..fc92ebba9 100644 --- a/app/views/articles/show.html.erb +++ b/app/views/articles/show.html.erb @@ -164,7 +164,9 @@
<%= @article.processed_html.html_safe %>
- <%= render "articles/collection", position: "bottom" %> + <% if @article.body_markdown && @article.body_markdown.size > 900 %> + <%= render "articles/collection", position: "bottom" %> + <% end %> <%= render 'articles/actions' %> <% if @article.body_markdown && @article.body_markdown.size > 900 %> diff --git a/lib/tasks/fetch.rake b/lib/tasks/fetch.rake index 291dca77e..7605f39b6 100644 --- a/lib/tasks/fetch.rake +++ b/lib/tasks/fetch.rake @@ -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 diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 5407331f7..3da93cab3 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -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 diff --git a/spec/requests/articles_api_spec.rb b/spec/requests/articles_api_spec.rb index bac52abcf..66f36d4a3 100644 --- a/spec/requests/articles_api_spec.rb +++ b/spec/requests/articles_api_spec.rb @@ -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 diff --git a/spec/requests/articles_create_spec.rb b/spec/requests/articles_create_spec.rb index 4df62ffe8..8ff304649 100644 --- a/spec/requests/articles_create_spec.rb +++ b/spec/requests/articles_create_spec.rb @@ -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