Scheduling articles (#17939)
* Article query spec for scheduled articles * Added scheduled article badge on the user dashboard * Added published_at field to editor options * Accept and validate published_at from editor * Refactor published_at validation * Allow 1-minute difference in published_at * Notice on an unpublished article page * Added specs for 'Click to edit' link on scheduled article preview page * ContextNotification model * Articles::Publish worker * Added specs for articles publish worker * Schedule publish articles worker * Added tests to check for scheduled posts in feeds * Don't allow managing scheduled articles * Don't send notifications for scheduled articles * Set published_at in Articles::Updater when publishing * Published_at value in post options * Pass timezone and set published_at accordingly * Limit setting published_at to the future * Readonly published_at for articles that were already published * Chagning published_at format in editor v1 (start) * Changed published_at format in frontmatter, specs * Added specs for updating published_at from frontmatter * Fixed accepting past published_at for articles published_from_feed * Enabled published_at validation: don't allow updating published_at for already published articles * Validate published_at on create * Added a spec for updating published_at for exported articles * Fixed specs related to creating articles with past published_at * Fixed specs related to past published_at for articles * Added a hack so that admins would be able to update published_at * Switch button text schedule/publish when changin publishedAt * Fixed saving published_at with timezone * Added a feature flag for scheduling articles * Default text in markdown editor depends on feature flag * Enable article editor cache again * Fixed the default value in the markdown editor * Fix sitemaps spec * Removed tooltip * Fixed articles update specs * Added missing locales * Fixed article create specs * Fixed spec * Removed commented code * Returned enabling extensions in the schema * Returned accidentally deleted constraint * Make articles query spec more stable Co-authored-by: Jeremy Friesen <jeremy.n.friesen@gmail.com> * Removed commented code * Removed unused code * A clearer policy Co-authored-by: Jeremy Friesen <jeremy.n.friesen@gmail.com> * Use StringInquirer for article current state * Added a note and todo to articles factory past trait * Remove duplicated PropType Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Refactor query in the Articles::PublishWorker Co-authored-by: Mac Siri <krairit.siri@gmail.com> * Refactor articleForm.jsx Co-authored-by: Mac Siri <krairit.siri@gmail.com> * Removed specs that are no longer relevant * Removed useless onKeyUp on a hidden input * Refactored articleForm * Hide scheduling from post options when published_at is readonly * Run sends notifications worker every 5 minutes instead of every minute Co-authored-by: Jeremy Friesen <jeremy.n.friesen@gmail.com> Co-authored-by: Suzanne Aitchison <suzanne@forem.com> Co-authored-by: Mac Siri <krairit.siri@gmail.com>
This commit is contained in:
parent
5e916b74c1
commit
ff76cdd3c5
62 changed files with 797 additions and 211 deletions
|
|
@ -40,7 +40,7 @@ module Admin
|
|||
def update
|
||||
article = Article.find(params[:id])
|
||||
|
||||
if article.update(article_params)
|
||||
if article.update(article_params.merge(admin_update: true))
|
||||
flash[:success] = I18n.t("admin.articles_controller.saved")
|
||||
else
|
||||
flash[:danger] = article.errors_as_sentence
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ class ArticlesController < ApplicationController
|
|||
|
||||
def create
|
||||
authorize Article
|
||||
|
||||
@user = current_user
|
||||
article = Articles::Creator.call(@user, article_params_json)
|
||||
|
||||
|
|
@ -160,7 +159,6 @@ class ArticlesController < ApplicationController
|
|||
def update
|
||||
authorize @article
|
||||
@user = @article.user || current_user
|
||||
|
||||
updated = Articles::Updater.call(@user, @article, article_params_json)
|
||||
|
||||
respond_to do |format|
|
||||
|
|
@ -303,6 +301,8 @@ class ArticlesController < ApplicationController
|
|||
# TODO: refactor all of this update logic into the Articles::Updater possibly,
|
||||
# ideally there should only be one place to handle the update logic
|
||||
def article_params_json
|
||||
return @article_params_json if @article_params_json
|
||||
|
||||
params.require(:article) # to trigger the correct exception in case `:article` is missing
|
||||
|
||||
params["article"].transform_keys!(&:underscore)
|
||||
|
|
@ -312,7 +312,7 @@ class ArticlesController < ApplicationController
|
|||
else
|
||||
%i[
|
||||
title body_markdown main_image published description video_thumbnail_url
|
||||
tag_list canonical_url series collection_id archived
|
||||
tag_list canonical_url series collection_id archived published_at timezone
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -325,7 +325,14 @@ class ArticlesController < ApplicationController
|
|||
allowed_params << :organization_id
|
||||
end
|
||||
|
||||
params.require(:article).permit(allowed_params)
|
||||
time_zone_str = params["article"].delete("timezone")
|
||||
if params["article"]["published_at"]
|
||||
time_zone = Time.find_zone(time_zone_str)
|
||||
time_zone ||= Time.find_zone("UTC")
|
||||
params["article"]["published_at"] = time_zone.parse(params["article"]["published_at"])
|
||||
end
|
||||
|
||||
@article_params_json = params.require(:article).permit(allowed_params)
|
||||
end
|
||||
|
||||
def allowed_to_change_org_id?
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ class StoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def permission_denied?
|
||||
!@article.published && params[:preview] != @article.password
|
||||
(!@article.published || @article.scheduled?) && params[:preview] != @article.password
|
||||
end
|
||||
|
||||
def assign_co_authors
|
||||
|
|
|
|||
|
|
@ -10,8 +10,19 @@ class ArticleDecorator < ApplicationDecorator
|
|||
DataInfo.to_json(object: cached_user, class_name: "User", id: user_id, style: "full")
|
||||
end
|
||||
|
||||
def current_state
|
||||
state = if !published?
|
||||
"unpublished"
|
||||
elsif scheduled?
|
||||
"scheduled"
|
||||
else
|
||||
"published"
|
||||
end
|
||||
ActiveSupport::StringInquirer.new(state)
|
||||
end
|
||||
|
||||
def current_state_path
|
||||
published ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
current_state.published? ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
end
|
||||
|
||||
def processed_canonical_url
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { getOSKeyboardModifierKeyString } from '@utilities/runtime';
|
|||
/* global activateRunkitTags */
|
||||
|
||||
/*
|
||||
Although the state fields: id, description, canonicalUrl, series, allSeries and
|
||||
Although the state fields: id, description, canonicalUrl, publishedAt, series, allSeries and
|
||||
editing are not used in this file, they are important to the
|
||||
editor.
|
||||
*/
|
||||
|
|
@ -63,6 +63,7 @@ export class ArticleForm extends Component {
|
|||
article: PropTypes.string.isRequired,
|
||||
organizations: PropTypes.string,
|
||||
siteLogo: PropTypes.string.isRequired,
|
||||
schedulingEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -71,7 +72,7 @@ export class ArticleForm extends Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { article, version, siteLogo } = this.props;
|
||||
const { article, version, siteLogo, schedulingEnabled } = this.props;
|
||||
let { organizations } = this.props;
|
||||
this.article = JSON.parse(article);
|
||||
organizations = organizations ? JSON.parse(organizations) : null;
|
||||
|
|
@ -102,10 +103,13 @@ export class ArticleForm extends Component {
|
|||
tagList: this.article.cached_tag_list || '',
|
||||
description: '', // eslint-disable-line react/no-unused-state
|
||||
canonicalUrl: this.article.canonical_url || '', // eslint-disable-line react/no-unused-state
|
||||
publishedAt: this.article.published_at || '', // eslint-disable-line react/no-unused-state
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '', // eslint-disable-line react/no-unused-state
|
||||
series: this.article.series || '', // eslint-disable-line react/no-unused-state
|
||||
allSeries: this.article.all_series || [], // eslint-disable-line react/no-unused-state
|
||||
bodyMarkdown: this.article.body_markdown || '',
|
||||
published: this.article.published || false,
|
||||
schedulingEnabled,
|
||||
previewShowing: false,
|
||||
previewLoading: false,
|
||||
previewResponse: { processed_html: '' },
|
||||
|
|
@ -331,6 +335,7 @@ export class ArticleForm extends Component {
|
|||
tagList: this.article.cached_tag_list || '',
|
||||
description: '', // eslint-disable-line react/no-unused-state
|
||||
canonicalUrl: this.article.canonical_url || '', // eslint-disable-line react/no-unused-state
|
||||
publishedAt: this.article.published_at || '', // eslint-disable-line react/no-unused-state
|
||||
series: this.article.series || '', // eslint-disable-line react/no-unused-state
|
||||
allSeries: this.article.all_series || [], // eslint-disable-line react/no-unused-state
|
||||
bodyMarkdown: this.article.body_markdown || '',
|
||||
|
|
@ -392,9 +397,11 @@ export class ArticleForm extends Component {
|
|||
tagList,
|
||||
bodyMarkdown,
|
||||
published,
|
||||
publishedAt,
|
||||
previewShowing,
|
||||
previewLoading,
|
||||
previewResponse,
|
||||
schedulingEnabled,
|
||||
submitting,
|
||||
organizations,
|
||||
organizationId,
|
||||
|
|
@ -491,6 +498,8 @@ export class ArticleForm extends Component {
|
|||
|
||||
<EditorActions
|
||||
published={published}
|
||||
publishedAt={publishedAt}
|
||||
schedulingEnabled={schedulingEnabled}
|
||||
version={version}
|
||||
onPublish={this.onPublish}
|
||||
onSaveDraft={this.onSaveDraft}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const EditorActions = ({
|
|||
onPublish,
|
||||
onClearChanges,
|
||||
published,
|
||||
publishedAt,
|
||||
schedulingEnabled,
|
||||
edited,
|
||||
version,
|
||||
passedData,
|
||||
|
|
@ -35,6 +37,16 @@ export const EditorActions = ({
|
|||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const publishedAtDate = publishedAt ? new Date(publishedAt) : now;
|
||||
const schedule = publishedAtDate > now;
|
||||
|
||||
const saveButtonText = schedule
|
||||
? 'Schedule'
|
||||
: published || isVersion1
|
||||
? 'Save changes'
|
||||
: 'Publish';
|
||||
|
||||
return (
|
||||
<div className="crayons-article-form__footer">
|
||||
<Button
|
||||
|
|
@ -43,7 +55,7 @@ export const EditorActions = ({
|
|||
onClick={onPublish}
|
||||
disabled={previewLoading}
|
||||
>
|
||||
{published || isVersion1 ? 'Save changes' : 'Publish'}
|
||||
{saveButtonText}
|
||||
</Button>
|
||||
|
||||
{!(published || isVersion1) && (
|
||||
|
|
@ -59,6 +71,7 @@ export const EditorActions = ({
|
|||
{isVersion2 && (
|
||||
<Options
|
||||
passedData={passedData}
|
||||
schedulingEnabled={schedulingEnabled}
|
||||
onConfigChange={onConfigChange}
|
||||
onSaveDraft={onSaveDraft}
|
||||
previewLoading={previewLoading}
|
||||
|
|
@ -82,6 +95,8 @@ EditorActions.propTypes = {
|
|||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
published: PropTypes.bool.isRequired,
|
||||
publishedAt: PropTypes.string.isRequired,
|
||||
schedulingEnabled: PropTypes.bool.isRequired,
|
||||
edited: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
onClearChanges: PropTypes.func.isRequired,
|
||||
|
|
|
|||
|
|
@ -11,19 +11,45 @@ import CogIcon from '@images/cog.svg';
|
|||
* @param {Function} props.onSaveDraft Callback for when the post draft is saved
|
||||
* @param {Function} props.onConfigChange Callback for when the config options have changed
|
||||
*/
|
||||
|
||||
function toISOStringLocal(datetime) {
|
||||
const month = (datetime.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = datetime.getDate().toString().padStart(2, '0');
|
||||
return [
|
||||
datetime.getFullYear(),
|
||||
'-',
|
||||
month,
|
||||
'-',
|
||||
day,
|
||||
'T',
|
||||
datetime.toLocaleTimeString(),
|
||||
].join('');
|
||||
}
|
||||
|
||||
export const Options = ({
|
||||
passedData: {
|
||||
published = false,
|
||||
publishedAt = '',
|
||||
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
allSeries = [],
|
||||
canonicalUrl = '',
|
||||
series = '',
|
||||
},
|
||||
schedulingEnabled,
|
||||
onSaveDraft,
|
||||
onConfigChange,
|
||||
previewLoading,
|
||||
}) => {
|
||||
let publishedField = '';
|
||||
let existingSeries = '';
|
||||
let publishedAtField = '';
|
||||
const publishedDate = new Date(publishedAt);
|
||||
const currentDate = new Date();
|
||||
|
||||
const localPublishedAt = toISOStringLocal(publishedDate);
|
||||
const minPublishedAt = toISOStringLocal(currentDate);
|
||||
|
||||
const readonlyPublishedAt = published && publishedDate < currentDate;
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
const seriesNames = allSeries.map((name, index) => {
|
||||
|
|
@ -66,6 +92,34 @@ export const Options = ({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
if (schedulingEnabled && !readonlyPublishedAt) {
|
||||
publishedAtField = (
|
||||
<div className="crayons-field mb-6">
|
||||
<label htmlFor="publishedAt" className="crayons-field__label">
|
||||
Schedule Publication
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
min={minPublishedAt}
|
||||
value={localPublishedAt} // "2022-04-28T15:00:00"
|
||||
className="crayons-textfield"
|
||||
name="publishedAt"
|
||||
onChange={onConfigChange}
|
||||
id="publishedAt"
|
||||
placeholder="..."
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
value={timezone} // "Asia/Magadan"
|
||||
className="crayons-textfield"
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
placeholder="..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="s:relative">
|
||||
<Button
|
||||
|
|
@ -104,6 +158,7 @@ export const Options = ({
|
|||
id="canonicalUrl"
|
||||
/>
|
||||
</div>
|
||||
{publishedAtField}
|
||||
<div className="crayons-field mb-6">
|
||||
<label htmlFor="series" className="crayons-field__label">
|
||||
Series
|
||||
|
|
@ -140,10 +195,13 @@ export const Options = ({
|
|||
Options.propTypes = {
|
||||
passedData: PropTypes.shape({
|
||||
published: PropTypes.bool.isRequired,
|
||||
publishedAt: PropTypes.string.isRequired,
|
||||
timezone: PropTypes.string.isRequired,
|
||||
allSeries: PropTypes.array.isRequired,
|
||||
canonicalUrl: PropTypes.string.isRequired,
|
||||
series: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
schedulingEnabled: PropTypes.bool.isRequired,
|
||||
onSaveDraft: PropTypes.func.isRequired,
|
||||
onConfigChange: PropTypes.func.isRequired,
|
||||
previewLoading: PropTypes.bool.isRequired,
|
||||
|
|
|
|||
|
|
@ -24,14 +24,15 @@ function loadForm() {
|
|||
window.csrfToken = csrfToken;
|
||||
|
||||
const root = document.querySelector('main');
|
||||
const { article, organizations, version, siteLogo } = root.dataset;
|
||||
|
||||
const { article, organizations, version, siteLogo, schedulingEnabled } =
|
||||
root.dataset;
|
||||
render(
|
||||
<ArticleForm
|
||||
article={article}
|
||||
organizations={organizations}
|
||||
version={version}
|
||||
siteLogo={siteLogo}
|
||||
schedulingEnabled={schedulingEnabled == 'true'}
|
||||
/>,
|
||||
root,
|
||||
root.firstElementChild,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ class Article < ApplicationRecord
|
|||
# @see Articles::CachedEntity caching strategy for entity attributes
|
||||
ATTRIBUTES_CACHED_FOR_RELATED_ENTITY = %i[name profile_image profile_image_url slug username].freeze
|
||||
|
||||
attr_accessor :publish_under_org
|
||||
# admin_update was added as a hack to bypass published_at validation when admin is updating
|
||||
# TODO: [@lightalloy] remove published_at validation from the model and
|
||||
# move it to the services where the create/update takes place to avoid using hacks
|
||||
attr_accessor :publish_under_org, :admin_update
|
||||
attr_writer :series
|
||||
|
||||
delegate :name, to: :user, prefix: true
|
||||
|
|
@ -99,6 +102,8 @@ class Article < ApplicationRecord
|
|||
has_many :mentions, as: :mentionable, inverse_of: :mentionable, dependent: :delete_all
|
||||
has_many :comments, as: :commentable, inverse_of: :commentable, dependent: :nullify
|
||||
has_many :context_notifications, as: :context, inverse_of: :context, dependent: :delete_all
|
||||
has_many :context_notifications_published, -> { where(context_notifications: { action: "Published" }) },
|
||||
as: :context, inverse_of: :context, class_name: "ContextNotification"
|
||||
has_many :html_variant_successes, dependent: :nullify
|
||||
has_many :html_variant_trials, dependent: :nullify
|
||||
has_many :notification_subscriptions, as: :notifiable, inverse_of: :notifiable, dependent: :delete_all
|
||||
|
|
@ -153,9 +158,10 @@ class Article < ApplicationRecord
|
|||
validates :video_source_url, url: { allow_blank: true, schemes: ["https"] }
|
||||
validates :video_state, inclusion: { in: %w[PROGRESSING COMPLETED] }, allow_nil: true
|
||||
validates :video_thumbnail_url, url: { allow_blank: true, schemes: %w[https http] }
|
||||
validate :future_or_current_published_at, on: :create
|
||||
validate :has_correct_published_at?, on: :update, unless: :admin_update
|
||||
|
||||
validate :canonical_url_must_not_have_spaces
|
||||
validate :past_or_present_date
|
||||
validate :validate_collection_permission
|
||||
validate :validate_tag
|
||||
validate :validate_video
|
||||
|
|
@ -164,7 +170,7 @@ class Article < ApplicationRecord
|
|||
validate :validate_co_authors_must_not_be_the_same, unless: -> { co_author_ids.blank? }
|
||||
validate :validate_co_authors_exist, unless: -> { co_author_ids.blank? }
|
||||
|
||||
before_validation :evaluate_markdown, :create_slug
|
||||
before_validation :evaluate_markdown, :create_slug, :set_published_date
|
||||
before_validation :remove_prohibited_unicode_characters
|
||||
before_validation :normalize_title
|
||||
before_save :set_cached_entities
|
||||
|
|
@ -174,7 +180,6 @@ class Article < ApplicationRecord
|
|||
before_save :fetch_video_duration
|
||||
before_save :set_caches
|
||||
before_create :create_password
|
||||
after_create :notify_slack_channel_about_publication
|
||||
after_update :notify_slack_channel_about_publication, if: -> { published && saved_change_to_published? }
|
||||
before_destroy :before_destroy_actions, prepend: true
|
||||
|
||||
|
|
@ -440,6 +445,10 @@ class Article < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def scheduled?
|
||||
published_at.future?
|
||||
end
|
||||
|
||||
def search_id
|
||||
"article_#{id}"
|
||||
end
|
||||
|
|
@ -476,7 +485,7 @@ class Article < ApplicationRecord
|
|||
end
|
||||
|
||||
def current_state_path
|
||||
published ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
published && !scheduled? ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}"
|
||||
end
|
||||
|
||||
def has_frontmatter?
|
||||
|
|
@ -714,7 +723,10 @@ class Article < ApplicationRecord
|
|||
self.title = front_matter["title"] if front_matter["title"].present?
|
||||
set_tag_list(front_matter["tags"]) if front_matter["tags"].present?
|
||||
self.published = front_matter["published"] if %w[true false].include?(front_matter["published"].to_s)
|
||||
self.published_at = parse_date(front_matter["date"]) if published
|
||||
|
||||
self.published_at = front_matter["published_at"] if front_matter["published_at"]
|
||||
self.published_at ||= parse_date(front_matter["date"]) if published
|
||||
|
||||
set_main_image(front_matter)
|
||||
self.canonical_url = front_matter["canonical_url"] if front_matter["canonical_url"].present?
|
||||
|
||||
|
|
@ -806,10 +818,21 @@ class Article < ApplicationRecord
|
|||
errors.add(:co_author_ids, I18n.t("models.article.invalid_coauthor"))
|
||||
end
|
||||
|
||||
def past_or_present_date
|
||||
return unless published_at && published_at > Time.current
|
||||
def future_or_current_published_at
|
||||
# allow published_at in the future or within 15 minutes in the past
|
||||
return if !published || published_at > Time.current - (15 * 60 * 60)
|
||||
|
||||
errors.add(:date_time, I18n.t("models.article.invalid_date"))
|
||||
errors.add(:published_at, I18n.t("models.article.future_or_current_published_at"))
|
||||
end
|
||||
|
||||
def has_correct_published_at?
|
||||
return unless published_at_was
|
||||
# don't allow editing published_at if an article has already been published
|
||||
# allow changes within one minute in case of editing via frontmatter w/o specifying seconds
|
||||
return unless published_was && published_at_was < Time.current &&
|
||||
changes["published_at"] && !(published_at_was - published_at).between?(-60, 60)
|
||||
|
||||
errors.add(:published_at, I18n.t("models.article.immutable_published_at"))
|
||||
end
|
||||
|
||||
def canonical_url_must_not_have_spaces
|
||||
|
|
@ -854,7 +877,6 @@ class Article < ApplicationRecord
|
|||
end
|
||||
|
||||
def set_all_dates
|
||||
set_published_date
|
||||
set_featured_number
|
||||
set_crossposted_at
|
||||
set_last_comment_at
|
||||
|
|
|
|||
|
|
@ -174,10 +174,6 @@ class ApplicationPolicy
|
|||
update?
|
||||
end
|
||||
|
||||
def manage?
|
||||
update? && record.published
|
||||
end
|
||||
|
||||
def destroy?
|
||||
false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ class ArticlePolicy < ApplicationPolicy
|
|||
user_author? || user_super_admin? || user_org_admin? || user_any_admin?
|
||||
end
|
||||
|
||||
def manage?
|
||||
update? && record.published? && !record.scheduled?
|
||||
end
|
||||
|
||||
def stats?
|
||||
require_user!
|
||||
user_author? || user_super_admin? || user_org_admin?
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ module Articles
|
|||
class Attributes
|
||||
ATTRIBUTES = %i[archived body_markdown canonical_url description
|
||||
edited_at main_image organization_id user_id published
|
||||
title video_thumbnail_url].freeze
|
||||
title video_thumbnail_url published_at].freeze
|
||||
|
||||
attr_reader :attributes, :article_user
|
||||
|
||||
|
|
@ -11,12 +11,17 @@ module Articles
|
|||
@article_user = article_user
|
||||
end
|
||||
|
||||
def for_update(update_edited_at: false)
|
||||
def for_update(update_edited_at: false, update_published_at: false)
|
||||
hash = attributes.slice(*ATTRIBUTES)
|
||||
# don't reset the collection when no series was passed
|
||||
hash[:collection] = collection if attributes.key?(:series)
|
||||
hash[:tag_list] = tag_list
|
||||
hash[:edited_at] = Time.current if update_edited_at
|
||||
if update_published_at
|
||||
hash[:published_at] = Time.current
|
||||
elsif hash[:published_at]
|
||||
hash[:published_at] = hash[:published_at].to_datetime
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -82,9 +82,11 @@ module Articles
|
|||
end
|
||||
|
||||
def user_editor_v1
|
||||
published_at = Time.current.strftime("%Y-%m-%d %H:%M %z")
|
||||
published_at_str = FeatureFlag.enabled?(:schedule_articles) ? "# published_at: #{published_at}\n" : ""
|
||||
body = "---\ntitle: \npublished: false\ndescription: " \
|
||||
"\ntags: \n# cover_image: https://direct_url_to_image.jpg" \
|
||||
"\n# Use a ratio of 100:42 for best results.\n---\n\n"
|
||||
"\n# Use a ratio of 100:42 for best results.\n#{published_at_str}---\n\n"
|
||||
|
||||
Article.new(
|
||||
body_markdown: body,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ module Articles
|
|||
# Subscribe author to notifications for all comments on their article.
|
||||
NotificationSubscription.create(user: user, notifiable_id: article.id, notifiable_type: "Article",
|
||||
config: "all_comments")
|
||||
|
||||
# Send notifications to any mentioned users, followed by any users who follow the article's author.
|
||||
Notification.send_to_mentioned_users_and_followers(article) if article.published?
|
||||
end
|
||||
|
||||
article
|
||||
|
|
|
|||
|
|
@ -17,18 +17,21 @@ module Articles
|
|||
|
||||
# updated edited time only if already published and not edited by an admin
|
||||
update_edited_at = article.user == user && article.published
|
||||
attrs = Articles::Attributes.new(article_params, article.user).for_update(update_edited_at: update_edited_at)
|
||||
# TODO: move setting published_at to Articles::Attributes
|
||||
# when publishing (draft => published), and published_at is nil or is in the past, set it to Time.current
|
||||
# except for exported articles
|
||||
publishing = !article.published && article_params[:published]
|
||||
past_published_at = !article_params[:published_at] || article_params[:published_at] < Time.current
|
||||
update_published_at = publishing && !article.published_from_feed && past_published_at
|
||||
attrs = Articles::Attributes.new(article_params, article.user)
|
||||
.for_update(update_edited_at: update_edited_at,
|
||||
update_published_at: update_published_at)
|
||||
|
||||
success = article.update(attrs)
|
||||
|
||||
if success
|
||||
user.rate_limiter.track_limit_by_action(:article_update)
|
||||
|
||||
if article.published && article.saved_change_to_published_at.present?
|
||||
# The first time that an article is published, we want to send notifications to any mentioned users first,
|
||||
# and then send notifications to any users who follow the article's author so as to avoid double mentions.
|
||||
Notification.send_to_mentioned_users_and_followers(article)
|
||||
elsif article.published
|
||||
if article.published && article.saved_change_to_published.blank?
|
||||
# If the article has already been published and is only being updated, then we need to create
|
||||
# mentions and send notifications to mentioned users inline via the Mentions::CreateAll service.
|
||||
Mentions::CreateAll.call(article)
|
||||
|
|
|
|||
|
|
@ -37,10 +37,11 @@
|
|||
</style>
|
||||
|
||||
<main id="main-content"
|
||||
data-article="<%= article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url updated_at], methods: %i[series all_series]) %>"
|
||||
data-article="<%= article.to_json(only: %i[id title cached_tag_list published published_at body_markdown main_image organization_id canonical_url updated_at], methods: %i[series all_series]) %>"
|
||||
data-organizations="<%= organizations&.to_json(only: %i[id name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
|
||||
data-version="<%= version %>"
|
||||
data-site-logo='<%= render "layouts/logo" %>'>
|
||||
data-site-logo='<%= render "layouts/logo" %>'
|
||||
data-scheduling-enabled="<%= FeatureFlag.enabled?(:schedule_articles) %>">
|
||||
<form class="crayons-article-form crayons-article-form--<%= version %>">
|
||||
|
||||
<div class="crayons-article-form__header">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<% title t("views.editor.heading.new") %>
|
||||
<%= content_for :page_meta do %>
|
||||
<link rel="canonical" href="<%= app_url("/new") %>" />
|
||||
<link rel="canonical" href="<%= app_url("/new") %>" />
|
||||
<% end %>
|
||||
|
||||
<% if user_signed_in? %>
|
||||
|
|
|
|||
|
|
@ -65,11 +65,11 @@
|
|||
|
||||
<main id="main-content" class="crayons-layout__content grid gap-4">
|
||||
<div class="article-wrapper">
|
||||
<% if !@article.published %>
|
||||
<% unless @article.current_state.published? %>
|
||||
<div class="crayons-notice crayons-notice--danger mb-4" aria-live="polite">
|
||||
<strong><%= t("views.articles.unpublished.subtitle") %></strong><%= t("views.articles.unpublished.text") %>
|
||||
<strong><%= t("views.articles.#{@article.current_state}.subtitle") %></strong><%= t("views.articles.#{@article.current_state}.text") %>
|
||||
<% if user_signed_in? %>
|
||||
<a id="author-click-to-edit" href="<%= @article.path %>/edit" class="fw-bold" style="display: none;"><%= t("views.articles.unpublished.edit") %></a>
|
||||
<a id="author-click-to-edit" href="<%= @article.path %>/edit" class="fw-bold" style="display: none;"><%= t("views.articles.#{@article.current_state}.edit") %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</h3>
|
||||
<% if article.published %>
|
||||
<p class="fs-s color-base-60 js-dashboard-story-details">
|
||||
<strong class="fw-medium"><%= t("views.dashboard.article.published") %></strong>
|
||||
<strong class="fw-medium"><%= t("views.dashboard.article.#{article.scheduled? ? 'scheduled_at' : 'published'}") %></strong>
|
||||
<%= local_date(article.published_timestamp, show_year: article.displayable_published_at.year != Time.current.year) %>
|
||||
<% if article.edited? %>
|
||||
<strong class="fw-medium pl-2"><%= t("views.dashboard.article.edited") %></strong>
|
||||
|
|
@ -31,6 +31,8 @@
|
|||
<div class="fs-s color-base-60">
|
||||
<% if !article.published? %>
|
||||
<a href="<%= article.path %>/edit" class="c-indicator c-indicator--warning"><%= t("views.dashboard.article.draft") %></a>
|
||||
<% elsif article.scheduled? %>
|
||||
<a href="<%= article.path %>/edit" class="c-indicator c-indicator--warning"><%= t("views.dashboard.article.scheduled") %></a>
|
||||
<% else %>
|
||||
<div class="flex flex-nowrap whitespace-nowrap" data-analytics-pageviews data-article-id="<%= article.id %>">
|
||||
<span class="flex items-center p-1" title="<%= t("views.dashboard.article.reactions.title") %>">
|
||||
|
|
@ -86,7 +88,7 @@
|
|||
<button class="crayons-btn crayons-btn--ghost crayons-btn--s" title="<%= t("views.dashboard.article.pin.title") %>"><%= t("views.dashboard.article.pin.text") %></button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% elsif article.published %>
|
||||
<% elsif article.published && !article.scheduled? %>
|
||||
<a href="<%= article.path %>/manage" class="crayons-btn crayons-btn--ghost crayons-btn--s" aria-label="<%= t("views.dashboard.article.manage.aria_label", title: article.title) %>"><%= t("views.dashboard.article.manage.text") %></a>
|
||||
<% elsif @user == article.user %>
|
||||
<a href="<%= article.path %>/delete_confirm" data-no-instant class="crayons-btn crayons-btn--ghost-danger crayons-btn--s"><%= t("views.dashboard.article.delete") %></a>
|
||||
|
|
|
|||
20
app/workers/articles/publish_worker.rb
Normal file
20
app/workers/articles/publish_worker.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
module Articles
|
||||
class PublishWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :medium_priority, lock: :until_executing
|
||||
|
||||
def perform
|
||||
# find published articles for which notifications were not set yet (so the context notifications were not created)
|
||||
published_articles = Article.where(published: true, published_at: 30.minutes.ago..Time.current)
|
||||
.where.missing(:context_notifications_published)
|
||||
published_articles.each do |article|
|
||||
# send slack notifications
|
||||
Slack::Messengers::ArticlePublished.call(article: article)
|
||||
# create mentions and notifications
|
||||
# Send notifications to any mentioned users, followed by any users who follow the article's author.
|
||||
Notification.send_to_mentioned_users_and_followers(article)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -21,6 +21,8 @@ en:
|
|||
must_not_have_spaces: must not have spaces
|
||||
published: Published
|
||||
unique_url: has already been taken. Email %{email} for further details.
|
||||
future_or_current_published_at: only future or current published_at allowed when publishing an article
|
||||
immutable_published_at: updating published_at for articles that have already been published is not allowed
|
||||
mention_too_many:
|
||||
one: You cannot mention more than %{count} users in a post!
|
||||
other: You cannot mention more than %{count} users in a post!
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ fr:
|
|||
must_not_have_spaces: must not have spaces
|
||||
published: Published
|
||||
unique_url: has already been taken. Email %{email} for further details.
|
||||
future_or_current_published_at: only future or current published_at allowed when publishing an article
|
||||
immutable_published_at: updating published_at for articles that have already been published is not allowed
|
||||
mention_too_many:
|
||||
one: You cannot mention more than %{count} users in a post!
|
||||
other: You cannot mention more than %{count} users in a post!
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ en:
|
|||
aria_label: Save to reading list
|
||||
initial: Save
|
||||
success: Saved
|
||||
scheduled:
|
||||
subtitle: Scheduled Post.
|
||||
text: " This URL is public but secret, so share at your own discretion."
|
||||
edit: Click to edit
|
||||
series:
|
||||
subtitle: "%{slug} (%{size})"
|
||||
size:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ fr:
|
|||
subtitle: Unpublished Post.
|
||||
text: " This URL is public but secret, so share at your own discretion."
|
||||
edit: Click to edit
|
||||
scheduled:
|
||||
subtitle: Scheduled Post.
|
||||
text: " This URL is public but secret, so share at your own discretion."
|
||||
edit: Click to edit
|
||||
co_authors_html: with %{names}
|
||||
crossposted:
|
||||
text_html: Originally published at %{url} %{on}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ en:
|
|||
series: 'Series:'
|
||||
series_html: "<strong>Series:</strong> %{series}"
|
||||
draft: Draft
|
||||
scheduled: Scheduled
|
||||
scheduled_at: 'Scheduled:'
|
||||
reactions:
|
||||
title: Reactions
|
||||
icon: Reactions
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ fr:
|
|||
series: 'Series:'
|
||||
series_html: "<strong>Series:</strong> %{series}"
|
||||
draft: Draft
|
||||
scheduled: Scheduled
|
||||
scheduled_at: 'Scheduled:'
|
||||
reactions:
|
||||
title: Reactions
|
||||
icon: Reactions
|
||||
|
|
|
|||
|
|
@ -146,3 +146,7 @@ capture_query_stats:
|
|||
description: "Collects Postgres query stats for PGHero (runs every 5 minutes)"
|
||||
cron: "*/5 * * * *"
|
||||
class: "CaptureQueryStatsWorker"
|
||||
notify_about_published_articles:
|
||||
description: "Sends notifications about the new published articles (runs every minute)"
|
||||
cron: "*/5 * * * *"
|
||||
class: "Articles::PublishWorker"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
module DataUpdateScripts
|
||||
class AddScheduleArticlesFeatureFlag
|
||||
def run
|
||||
FeatureFlag.add(:schedule_articles)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -45,6 +45,12 @@ RSpec.describe ArticleDecorator, type: :decorator do
|
|||
expected_result = "/#{article.username}/#{article.slug}?preview=#{article.password}"
|
||||
expect(article.current_state_path).to eq(expected_result)
|
||||
end
|
||||
|
||||
it "returns the path /:username/:slug?:password when scheduled" do
|
||||
article = create_article(published: true, published_at: Date.tomorrow)
|
||||
expected_result = "/#{article.username}/#{article.slug}?preview=#{article.password}"
|
||||
expect(article.current_state_path).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "has_recent_comment_activity?" do
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ FactoryBot.define do
|
|||
with_user_subscription_tag { false }
|
||||
with_title { true }
|
||||
with_collection { nil }
|
||||
past_published_at { Time.current }
|
||||
end
|
||||
co_author_ids { [] }
|
||||
association :user, factory: :user, strategy: :create
|
||||
|
|
@ -70,4 +71,13 @@ FactoryBot.define do
|
|||
trait :with_discussion_lock do
|
||||
after(:create) { |article| create(:discussion_lock, locking_user_id: article.user_id, article_id: article.id) }
|
||||
end
|
||||
|
||||
# NOTE: [@lightalloy] This trait is used to create articles published in the past (with past published_at)
|
||||
# we can't do it directly because of the validation Article#has_correct_published_at?
|
||||
# TODO: [@lightalloy] Remove the trait and its usage when has_correct_published_at? will be removed
|
||||
trait :past do
|
||||
after(:create) do |article, evaluator|
|
||||
article.update_column(:published_at, evaluator.past_published_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
require "rails_helper"
|
||||
require Rails.root.join(
|
||||
"lib/data_update_scripts/20220613093333_add_schedule_articles_feature_flag.rb",
|
||||
)
|
||||
|
||||
describe DataUpdateScripts::AddScheduleArticlesFeatureFlag do
|
||||
after do
|
||||
FeatureFlag.remove(:schedule_articles)
|
||||
end
|
||||
|
||||
it "adds the :schedule_articles flag" do
|
||||
expect do
|
||||
described_class.new.run
|
||||
end.to change { FeatureFlag.exist?(:schedule_articles) }.from(false).to(true)
|
||||
end
|
||||
|
||||
it "works if the flag is already available" do
|
||||
FeatureFlag.add(:schedule_articles)
|
||||
|
||||
expect do
|
||||
described_class.new.run
|
||||
end.not_to change { FeatureFlag.exist?(:schedule_articles) }
|
||||
end
|
||||
end
|
||||
|
|
@ -52,16 +52,16 @@ RSpec.describe AbExperiment::GoalConversionHandler do
|
|||
end
|
||||
|
||||
it "records a conversion", :aggregate_failures do
|
||||
create(:article, published_at: 2.days.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 2.days.ago, user_id: user.id)
|
||||
handler.call
|
||||
expect(FieldTest::Event.last.field_test_membership.participant_id).to eq(user.id.to_s)
|
||||
expect(FieldTest::Event.last.name).to eq(goal)
|
||||
end
|
||||
|
||||
it "records weekly post publishing conversions", :aggregate_failures do
|
||||
create(:article, published_at: 2.days.ago, user_id: user.id)
|
||||
create(:article, published_at: 3.days.ago, user_id: user.id)
|
||||
create(:article, published_at: 13.days.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 2.days.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 3.days.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 13.days.ago, user_id: user.id)
|
||||
|
||||
handler.call
|
||||
|
||||
|
|
@ -75,10 +75,10 @@ RSpec.describe AbExperiment::GoalConversionHandler do
|
|||
end
|
||||
|
||||
it "records a conversion when they post 4 within a week" do
|
||||
create(:article, published_at: 25.hours.ago, user_id: user.id)
|
||||
create(:article, published_at: 49.hours.ago, user_id: user.id)
|
||||
create(:article, published_at: 73.hours.ago, user_id: user.id)
|
||||
create(:article, published_at: 97.hours.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 25.hours.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 49.hours.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 73.hours.ago, user_id: user.id)
|
||||
create(:article, :past, past_published_at: 97.hours.ago, user_id: user.id)
|
||||
|
||||
handler.call
|
||||
|
||||
|
|
|
|||
|
|
@ -484,23 +484,33 @@ RSpec.describe Article, type: :model do
|
|||
end
|
||||
|
||||
it "sets published_at from a valid frontmatter date" do
|
||||
date = (Date.current - 5.days).strftime("%d/%m/%Y")
|
||||
date = (Date.current + 5.days).strftime("%d/%m/%Y")
|
||||
article_with_date = build(:article, with_date: true, date: date, published_at: nil)
|
||||
expect(article_with_date.valid?).to be(true)
|
||||
expect(article_with_date.published_at.strftime("%d/%m/%Y")).to eq(date)
|
||||
end
|
||||
|
||||
it "rejects future dates set from frontmatter" do
|
||||
invalid_article = build(:article, with_date: true, date: Date.tomorrow.strftime("%d/%m/%Y"), published_at: nil)
|
||||
expect(invalid_article.valid?).to be(false)
|
||||
expect(invalid_article.errors[:date_time])
|
||||
.to include("must be entered in DD/MM/YYYY format with current or past date")
|
||||
it "sets published_at from frontmatter" do
|
||||
published_at = (Date.current + 10.days).strftime("%d/%m/%Y %H:%M")
|
||||
body_markdown = "---\ntitle: Title\npublished: false\npublished_at: #{published_at}\ndescription:\ntags: heytag
|
||||
\n---\n\nHey this is the article"
|
||||
article_with_published_at = build(:article, body_markdown: body_markdown)
|
||||
expect(article_with_published_at.valid?).to be(true)
|
||||
expect(article_with_published_at.published_at.strftime("%d/%m/%Y %H:%M")).to eq(published_at)
|
||||
end
|
||||
|
||||
it "rejects future dates even when it's published at" do
|
||||
article.published_at = Date.tomorrow
|
||||
expect(article.valid?).to be(false)
|
||||
expect(article.errors[:date_time]).to include("must be entered in DD/MM/YYYY format with current or past date")
|
||||
it "doesn't allow past published_at when publishing on create" do
|
||||
article2 = build(:article, published_at: 10.days.ago, published: true)
|
||||
expect(article2.valid?).to be false
|
||||
expect(article2.errors[:published_at])
|
||||
.to include("only future or current published_at allowed when publishing an article")
|
||||
end
|
||||
|
||||
it "doesn't allow updating published_at if an article has already been published" do
|
||||
article.published_at = (Date.current + 10.days).strftime("%d/%m/%Y %H:%M")
|
||||
expect(article.valid?).to be false
|
||||
expect(article.errors[:published_at])
|
||||
.to include("updating published_at for articles that have already been published is not allowed")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1167,8 +1177,9 @@ RSpec.describe Article, type: :model do
|
|||
sidekiq_perform_enqueued_jobs(only: Slack::Messengers::Worker)
|
||||
end
|
||||
|
||||
it "queues a slack message to be sent for a new published article" do
|
||||
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
|
||||
# slack messages will be queued in a publish worker
|
||||
it "doesn't queue a slack message to be sent for a new published article" do
|
||||
sidekiq_assert_no_enqueued_jobs(only: Slack::Messengers::Worker) do
|
||||
create(:article, user: user, published: true, published_at: Time.current)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ RSpec.describe Articles::ActiveThreadsQuery, type: :query do
|
|||
context "when time_ago is latest" do
|
||||
it "returns the latest article with a good score", :aggregate_failures do
|
||||
article = create(:article, tags: "discuss", score: described_class::MINIMUM_SCORE + 1)
|
||||
create(:article, published_at: 2.days.ago, tags: "discuss", score: described_class::MINIMUM_SCORE + 1)
|
||||
create(:article, :past, past_published_at: 2.days.ago, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE + 1)
|
||||
|
||||
result = described_class.call(tags: "discuss", time_ago: "latest", count: 10)
|
||||
expect(result.length).to eq(2)
|
||||
|
|
@ -28,11 +29,11 @@ RSpec.describe Articles::ActiveThreadsQuery, type: :query do
|
|||
context "when given a precise time" do
|
||||
it "returns article ordered_by comment_count based on time", :aggregate_failures do
|
||||
time = 2.days.ago
|
||||
article = create(:article, comments_count: 20, published_at: time, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
article = create(:article, :past, comments_count: 20, past_published_at: time, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
create(:article, comments_count: 10, tags: "discuss", score: described_class::MINIMUM_SCORE)
|
||||
create(:article, published_at: time - 2.days, comments_count: 30, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
create(:article, :past, past_published_at: time - 2.days, comments_count: 30, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
|
||||
result = described_class.call(tags: "discuss", time_ago: time, count: 10)
|
||||
expect(result.length).to eq(2)
|
||||
|
|
@ -41,8 +42,8 @@ RSpec.describe Articles::ActiveThreadsQuery, type: :query do
|
|||
|
||||
it "returns any article when no higher-quality article could be found", :aggregate_failures do
|
||||
time = 2.days.ago
|
||||
article = create(:article, comments_count: 20, published_at: time - 5.days, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
article = create(:article, :past, comments_count: 20, past_published_at: time - 5.days, tags: "discuss",
|
||||
score: described_class::MINIMUM_SCORE)
|
||||
create(:article, comments_count: 10, tags: "discuss", score: described_class::MINIMUM_SCORE - 10)
|
||||
|
||||
result = described_class.call(tags: "discuss", time_ago: time, count: 10)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ RSpec.describe Homepage::ArticlesQuery, type: :query do
|
|||
expect(described_class.call.ids).to eq([article.id])
|
||||
end
|
||||
|
||||
it "does not return scheduled articles" do
|
||||
scheduled_article = create(:article, published_at: Date.current + 5.days)
|
||||
|
||||
expect(described_class.call.ids).not_to include(scheduled_article.id)
|
||||
end
|
||||
|
||||
it "does not return draft articles" do
|
||||
article = create(:article, published: false, published_at: nil)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ RSpec.describe "/admin", type: :request do
|
|||
xit { is_expected.to include("Apr 23") }
|
||||
|
||||
xit "displays correct number of posts from past week" do
|
||||
create(:article, published_at: Time.zone.today)
|
||||
create(:article, published_at: Time.current)
|
||||
create(:article, published_at: 1.day.ago)
|
||||
create(:article, published_at: 7.days.ago)
|
||||
create(:article, published_at: 8.days.ago)
|
||||
|
|
@ -51,7 +51,7 @@ RSpec.describe "/admin", type: :request do
|
|||
end
|
||||
|
||||
it "displays correct number of comments from past week" do
|
||||
create(:comment, created_at: Time.zone.today)
|
||||
create(:comment, created_at: Time.current)
|
||||
create(:comment, created_at: 1.day.ago)
|
||||
create(:comment, created_at: 8.days.ago)
|
||||
get admin_path
|
||||
|
|
@ -60,7 +60,7 @@ RSpec.describe "/admin", type: :request do
|
|||
end
|
||||
|
||||
it "displays correct number of reactions from past week" do
|
||||
create(:reaction, created_at: Time.zone.today)
|
||||
create(:reaction, created_at: Time.current)
|
||||
create(:reaction, created_at: 3.days.ago)
|
||||
create(:reaction, created_at: 2.weeks.ago)
|
||||
get admin_path
|
||||
|
|
@ -69,7 +69,7 @@ RSpec.describe "/admin", type: :request do
|
|||
end
|
||||
|
||||
it "displays correct number of new members from past week" do
|
||||
create(:user, registered_at: Time.zone.today)
|
||||
create(:user, registered_at: Time.current)
|
||||
create(:user, registered_at: 2.days.ago)
|
||||
create(:user, registered_at: 10.days.ago)
|
||||
get admin_path
|
||||
|
|
@ -78,7 +78,7 @@ RSpec.describe "/admin", type: :request do
|
|||
end
|
||||
|
||||
it "does not display data from previous weeks", :aggregate_failures do
|
||||
create(:article, published_at: 8.days.ago)
|
||||
create(:article, :past, past_published_at: 8.days.ago)
|
||||
create(:comment, created_at: 2.weeks.ago)
|
||||
create(:reaction, created_at: 1.month.ago)
|
||||
create(:user, registered_at: 10.days.ago)
|
||||
|
|
@ -91,10 +91,10 @@ RSpec.describe "/admin", type: :request do
|
|||
end
|
||||
|
||||
it "does not display data from today", :aggregate_failures do
|
||||
create(:article, published_at: Time.zone.today)
|
||||
create(:comment, created_at: Time.zone.today)
|
||||
create(:reaction, created_at: Time.zone.today)
|
||||
create(:user, registered_at: Time.zone.today)
|
||||
create(:article, published_at: Time.current)
|
||||
create(:comment, created_at: Time.current)
|
||||
create(:reaction, created_at: Time.current)
|
||||
create(:user, registered_at: Time.current)
|
||||
get admin_path
|
||||
|
||||
expect(body).to include "0</span> Posts"
|
||||
|
|
|
|||
|
|
@ -1075,27 +1075,6 @@ RSpec.describe "Api::V0::Articles", type: :request do
|
|||
expect(article.reload.published).to be(true)
|
||||
end
|
||||
|
||||
it "sends a notification when the article gets published" do
|
||||
expect(article.published).to be(false)
|
||||
allow(Notification).to receive(:send_to_followers)
|
||||
put_article(body_markdown: "Yo ho ho", published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(Notification).to have_received(:send_to_followers).with(article, "Published").once
|
||||
end
|
||||
|
||||
it "only sends a notification the first time the article gets published" do
|
||||
expect(article.published).to be(false)
|
||||
allow(Notification).to receive(:send_to_followers)
|
||||
put_article(body_markdown: "Yo ho ho", published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
article.update_columns(published: false)
|
||||
put_article(published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(Notification).to have_received(:send_to_followers).with(article, "Published").once
|
||||
end
|
||||
|
||||
it "does not update the editing time when updated before publication" do
|
||||
article.update_columns(edited_at: nil)
|
||||
expect(article.published).to be(false)
|
||||
|
|
@ -1108,7 +1087,7 @@ RSpec.describe "Api::V0::Articles", type: :request do
|
|||
end
|
||||
|
||||
it "updates the editing time when updated after publication" do
|
||||
article.update_columns(published: true)
|
||||
article.update_columns(published: true, published_at: Time.current)
|
||||
put_article(
|
||||
title: Faker::Book.title,
|
||||
body_markdown: "Yo ho ho",
|
||||
|
|
@ -1141,7 +1120,7 @@ RSpec.describe "Api::V0::Articles", type: :request do
|
|||
|
||||
it "updates the editing time when updated after publication if the owner is an admin" do
|
||||
user.add_role(:super_admin)
|
||||
article.update_columns(edited_at: nil, published: true)
|
||||
article.update_columns(edited_at: nil, published: true, published_at: Time.current)
|
||||
put_article(
|
||||
title: Faker::Book.title,
|
||||
body_markdown: "Yo ho ho",
|
||||
|
|
|
|||
|
|
@ -1089,27 +1089,6 @@ RSpec.describe "Api::V1::Articles", type: :request do
|
|||
expect(article.reload.published).to be(true)
|
||||
end
|
||||
|
||||
it "sends a notification when the article gets published" do
|
||||
expect(article.published).to be(false)
|
||||
allow(Notification).to receive(:send_to_followers)
|
||||
put_article(body_markdown: "Yo ho ho", published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(Notification).to have_received(:send_to_followers).with(article, "Published").once
|
||||
end
|
||||
|
||||
it "only sends a notification the first time the article gets published" do
|
||||
expect(article.published).to be(false)
|
||||
allow(Notification).to receive(:send_to_followers)
|
||||
put_article(body_markdown: "Yo ho ho", published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
article.update_columns(published: false)
|
||||
put_article(published: true)
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
expect(Notification).to have_received(:send_to_followers).with(article, "Published").once
|
||||
end
|
||||
|
||||
it "does not update the editing time when updated before publication" do
|
||||
article.update_columns(edited_at: nil)
|
||||
expect(article.published).to be(false)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ require "rails_helper"
|
|||
RSpec.describe "ArticlesCreate", type: :request do
|
||||
let(:user) { create(:user, :org_member) }
|
||||
let(:template) { file_fixture("article_published.txt").read }
|
||||
let(:new_title) { "NEW TITLE #{rand(100)}" }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "creates ordinary article with proper params" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
post "/articles", params: {
|
||||
article: { title: new_title, body_markdown: "Yo ho ho#{rand(100)}", tag_list: "yo" }
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ RSpec.describe "ArticlesCreate", type: :request do
|
|||
end
|
||||
|
||||
it "properly downcase tags" do
|
||||
new_title = "NEW TITLE #{rand(100)}"
|
||||
post "/articles", params: {
|
||||
article: { title: new_title, body_markdown: "Yo ho ho#{rand(100)}", tag_list: "What" }
|
||||
}
|
||||
|
|
@ -114,4 +113,66 @@ RSpec.describe "ArticlesCreate", type: :request do
|
|||
expect(response.headers["Retry-After"]).to eq(expected_retry_after)
|
||||
end
|
||||
end
|
||||
|
||||
context "when setting published_at in editor v2" do
|
||||
let(:tomorrow) { 1.day.from_now }
|
||||
let(:attributes) do
|
||||
{ title: new_title, body_markdown: "Yo ho ho#{rand(100)}",
|
||||
published_at: "#{tomorrow.strftime('%Y-%m-%d')} 18:00" }
|
||||
end
|
||||
|
||||
it "sets published_at according to the timezone" do
|
||||
attributes[:timezone] = "Europe/Moscow"
|
||||
post "/articles", params: { article: attributes }
|
||||
a = Article.find_by(title: new_title)
|
||||
published_at_utc = a.published_at.in_time_zone("UTC").strftime("%m/%d/%Y %H:%M")
|
||||
expect(published_at_utc).to eq("#{tomorrow.strftime('%m/%d/%Y')} 15:00")
|
||||
end
|
||||
|
||||
# crossing the date line
|
||||
it "sets published_at for another timezone" do
|
||||
attributes[:timezone] = "Pacific/Honolulu"
|
||||
post "/articles", params: { article: attributes }
|
||||
a = Article.find_by(title: new_title)
|
||||
published_at_utc = a.published_at.in_time_zone("UTC").strftime("%m/%d/%Y %H:%M")
|
||||
expect(published_at_utc).to eq("#{(tomorrow + 1.day).strftime('%m/%d/%Y')} 04:00")
|
||||
end
|
||||
end
|
||||
|
||||
context "when setting published_at from editor v1" do
|
||||
it "sets current published_at when publishing and published_at not specified" do
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\n---\n\nHey this is the article"
|
||||
post "/articles", params: { article: { body_markdown: body_markdown } }
|
||||
a = Article.find_by(title: "super-article")
|
||||
expect(a.published_at).to be_within(1.minute).of(Time.current)
|
||||
end
|
||||
|
||||
it "doesn't set published_at for drafts when published_at is not specified" do
|
||||
body_markdown = "---\ntitle: super-article\npublished: false\ndescription:\ntags: heytag
|
||||
\n---\n\nHey this is the article"
|
||||
post "/articles", params: { article: { body_markdown: body_markdown } }
|
||||
a = Article.find_by(title: "super-article")
|
||||
expect(a.published_at).to be_nil
|
||||
end
|
||||
|
||||
it "sets published_at from frontmatter" do
|
||||
published_at = 10.days.from_now.in_time_zone("UTC")
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\npublished_at: #{published_at.strftime('%Y-%m-%d %H:%M %z')}\n---\n\nHey this is the article"
|
||||
post "/articles", params: { article: { body_markdown: body_markdown } }
|
||||
a = Article.find_by(title: "super-article")
|
||||
expect(a.published_at).to be_within(1.minute).of(published_at)
|
||||
end
|
||||
|
||||
it "sets published_at with timezone from frontmatter" do
|
||||
published_at = 10.days.from_now.in_time_zone("America/Caracas")
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\npublished_at: #{published_at.strftime('%Y-%m-%d %H:%M %z')}\n---\n\nHey this is the article"
|
||||
post "/articles", params: { article: { body_markdown: body_markdown } }
|
||||
a = Article.find_by(title: "super-article")
|
||||
# binding.pry
|
||||
expect(a.published_at).to be_within(1.minute).of(published_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,6 +59,27 @@ RSpec.describe "ArticlesShow", type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe "GET /:username/:slug (scheduled)" do
|
||||
let(:scheduled_article) { create(:article, published: true, published_at: Date.tomorrow) }
|
||||
let(:query_params) { "?preview=#{scheduled_article.password}" }
|
||||
let(:scheduled_article_path) { scheduled_article.path + query_params }
|
||||
|
||||
it "renders a scheduled article with the article password" do
|
||||
get scheduled_article_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include(scheduled_article.title)
|
||||
end
|
||||
|
||||
it "doesn't show edit link when user is not signed in" do
|
||||
get scheduled_article_path
|
||||
expect(response.body).not_to include("Click to edit")
|
||||
end
|
||||
|
||||
it "renders 404 for a scheduled article w/o article password" do
|
||||
expect { get scheduled_article.path }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
it "renders the proper organization for an article when one is present" do
|
||||
get organization.path
|
||||
expect(response_json).to include(
|
||||
|
|
|
|||
|
|
@ -311,6 +311,16 @@ RSpec.describe "Articles", type: :request do
|
|||
expect(response.body).to include("Manage Your Post")
|
||||
end
|
||||
|
||||
it "returns unauthorized for a draft" do
|
||||
draft = create(:article, published: false, user: user)
|
||||
expect { get "#{draft.path}/manage" }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
|
||||
it "returns unauthorized for a scheduled article" do
|
||||
scheduled_article = create(:article, published: true, user: user, published_at: 1.day.from_now)
|
||||
expect { get "#{scheduled_article.path}/manage" }.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
|
||||
it "returns unauthorized if the user is not the author" do
|
||||
second_user = create(:user)
|
||||
article = create(:article, user: second_user)
|
||||
|
|
|
|||
|
|
@ -129,20 +129,11 @@ RSpec.describe "ArticlesUpdate", type: :request do
|
|||
expect(article.collection).to be_nil
|
||||
end
|
||||
|
||||
it "creates a notification job if published the first time" do
|
||||
draft = create(:article, published: false, published_at: nil, user_id: user.id)
|
||||
sidekiq_assert_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
put "/articles/#{draft.id}", params: {
|
||||
article: { published: true, body_markdown: "blah" }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
it "does not create a notification job if published the second time" do
|
||||
article.update_column(:published, false)
|
||||
sidekiq_assert_not_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
put "/articles/#{article.id}", params: {
|
||||
article: { published: true, body_markdown: "blah" }
|
||||
article: { published: true, body_markdown: "blah" }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -167,4 +158,88 @@ RSpec.describe "ArticlesUpdate", type: :request do
|
|||
expect(response).to redirect_to "#{article.path}/edit"
|
||||
expect(article.reload.video_thumbnail_url).to include "https://i.imgur.com/HPiu7N4.jpg"
|
||||
end
|
||||
|
||||
context "when setting published_at in editor v2" do
|
||||
let(:tomorrow) { 1.day.from_now }
|
||||
let(:published_at) { "#{tomorrow.strftime('%d.%m.%Y')} 18:00" }
|
||||
let(:attributes) do
|
||||
{ title: "NEW TITLE #{rand(100)}", body_markdown: "Yo ho ho#{rand(100)}", published_at: published_at }
|
||||
end
|
||||
|
||||
# scheduled => scheduled
|
||||
it "updates published_at from scheduled to scheduled" do
|
||||
article.update_column(:published_at, 3.days.from_now)
|
||||
attributes[:timezone] = "Europe/Moscow"
|
||||
put "/articles/#{article.id}", params: { article: attributes }
|
||||
article.reload
|
||||
published_at_utc = article.published_at.in_time_zone("UTC").strftime("%m/%d/%Y %H:%M")
|
||||
expect(published_at_utc).to eq("#{tomorrow.strftime('%m/%d/%Y')} 15:00")
|
||||
end
|
||||
|
||||
# draft => scheduled
|
||||
it "sets published_at according to the timezone when updating draft => scheduled" do
|
||||
draft = create(:article, published: false, user_id: user.id)
|
||||
attributes[:published] = true
|
||||
attributes[:published_at] = "#{tomorrow.strftime('%d/%m/%Y')} 18:00"
|
||||
attributes[:timezone] = "America/Mexico_City"
|
||||
put "/articles/#{draft.id}", params: { article: attributes }
|
||||
draft.reload
|
||||
published_at_utc = draft.published_at.in_time_zone("UTC").strftime("%m/%d/%Y %H:%M")
|
||||
expect(published_at_utc).to eq("#{tomorrow.strftime('%m/%d/%Y')} 23:00")
|
||||
expect(draft.published).to be true
|
||||
end
|
||||
|
||||
it "doesn't update published_at when published => published" do
|
||||
published_at = DateTime.parse("2022-01-01 15:00 -0400")
|
||||
article.update_column(:published_at, published_at)
|
||||
attributes[:timezone] = "Europe/Moscow"
|
||||
put "/articles/#{article.id}", params: { article: attributes }
|
||||
article.reload
|
||||
expect(article.published_at).to eq(published_at)
|
||||
end
|
||||
end
|
||||
|
||||
context "when setting published_at in editor v1" do
|
||||
it "updates published_at from scheduled to scheduled with timezone" do
|
||||
published_at = 3.days.from_now.in_time_zone("Asia/Dhaka")
|
||||
article.update_columns(published: true, published_at: 1.day.from_now)
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\npublished_at: #{published_at.strftime('%Y-%m-%d %H:%M %z')}\n---\n\nHey this is the article"
|
||||
|
||||
put "/articles/#{article.id}", params: { article: { body_markdown: body_markdown } }
|
||||
article.reload
|
||||
expect(article.published_at).to be_within(1.minute).of(published_at)
|
||||
end
|
||||
|
||||
it "doesn't update published_at when published => published" do
|
||||
published_at = DateTime.parse("2022-05-23 18:00 +0030")
|
||||
article.update_columns(published: true, published_at: published_at)
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\npublished_at: #{1.day.from_now.strftime('%Y-%m-%d %H:%M %z')}\n---\n\nHey this is the article"
|
||||
|
||||
put "/articles/#{article.id}", params: { article: { body_markdown: body_markdown } }
|
||||
article.reload
|
||||
expect(article.published_at).to eq(published_at)
|
||||
end
|
||||
|
||||
it "sets current published_at when draft => published and no published_at specified" do
|
||||
draft = create(:article, published: false, user_id: user.id, published_at: nil)
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\n---\n\nHey this is the article"
|
||||
put "/articles/#{draft.id}", params: { article: { body_markdown: body_markdown } }
|
||||
draft.reload
|
||||
expect(draft.published_at).to be_within(1.minute).of(Time.current)
|
||||
end
|
||||
|
||||
it "allows to set past published_at when publishing with date and no published_at for exported articles" do
|
||||
date = "2022-05-02 19:00:30 UTC"
|
||||
draft = create(:article, published: false, user_id: user.id, published_from_feed: true, published_at: nil)
|
||||
body_markdown = "---\ntitle: super-article\npublished: true\ndescription:\ntags: heytag
|
||||
\ndate: #{date}---\n\nHey this is the article"
|
||||
put "/articles/#{draft.id}", params: { article: { body_markdown: body_markdown } }
|
||||
draft.reload
|
||||
expect(draft.published).to be true
|
||||
expect(draft.published_at).to be_within(1.minute).of(DateTime.parse(date))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RSpec.describe "Dashboards", type: :request do
|
|||
let(:super_admin) { create(:user, :super_admin) }
|
||||
let(:article) { create(:article, user: user) }
|
||||
let(:unpublished_article) { create(:article, user: user, published: false) }
|
||||
let(:scheduled_article) { create(:article, user: user, published_at: 2.days.from_now) }
|
||||
let(:organization) { create(:organization) }
|
||||
|
||||
describe "GET /dashboard" do
|
||||
|
|
@ -41,6 +42,24 @@ RSpec.describe "Dashboards", type: :request do
|
|||
expect(response.body).to include "Delete"
|
||||
end
|
||||
|
||||
it "renders the draft state indicator" do
|
||||
unpublished_article
|
||||
get "/dashboard"
|
||||
expect(response.body).to include "Draft"
|
||||
end
|
||||
|
||||
it "renders scheduled state indicator" do
|
||||
scheduled_article
|
||||
get "/dashboard"
|
||||
expect(response.body).to include "Scheduled"
|
||||
end
|
||||
|
||||
it "renders the delete button for scheduled article" do
|
||||
scheduled_article
|
||||
get "/dashboard"
|
||||
expect(response.body).to include "Delete"
|
||||
end
|
||||
|
||||
it "renders subscriptions for articles with subscriptions" do
|
||||
allow(user).to receive(:has_role?).and_call_original
|
||||
allow(user).to receive(:has_role?).with(:restricted_liquid_tag,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe "Sitemaps", type: :request do
|
|||
it "sends a surrogate key (for Fastly's user)" do
|
||||
articles = create_list(:article, 4)
|
||||
included_articles = articles.first(3)
|
||||
included_articles.each { |a| a.update(published_at: "2020-03-07T00:27:30Z", score: 10) }
|
||||
included_articles.each { |a| a.update_columns(published_at: "2020-03-07T00:27:30Z", score: 10) }
|
||||
|
||||
get "/sitemap-Mar-2020.xml"
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ RSpec.describe "Stories::TaggedArticlesIndex", type: :request do
|
|||
let(:tag) { create(:tag) }
|
||||
let(:org) { create(:organization) }
|
||||
let(:article) { create(:article, tags: tag.name, score: 5) }
|
||||
let(:unsupported_tag) { create(:tag, supported: false) }
|
||||
|
||||
before do
|
||||
stub_const("Stories::TaggedArticlesController::SIGNED_OUT_RECORD_COUNT", 10)
|
||||
|
|
@ -62,8 +63,8 @@ RSpec.describe "Stories::TaggedArticlesIndex", type: :request do
|
|||
end
|
||||
|
||||
it "renders page when tag is not supported but has at least one approved article" do
|
||||
unsupported_tag = create(:tag, supported: false)
|
||||
create(:article, published: true, approved: true, tags: unsupported_tag, published_at: 5.years.ago)
|
||||
create(:article, :past, published: true, approved: true, tags: unsupported_tag,
|
||||
past_published_at: 5.years.ago)
|
||||
|
||||
get "/t/#{unsupported_tag.name}/top/week"
|
||||
|
||||
|
|
@ -85,6 +86,11 @@ RSpec.describe "Stories::TaggedArticlesIndex", type: :request do
|
|||
expect { get "/t/#{tag.name}" }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "renders not found if there are approved but scheduled posts" do
|
||||
create(:article, published: true, approved: true, tags: unsupported_tag, published_at: 1.hour.from_now)
|
||||
expect { get "/t/#{unsupported_tag.name}" }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "renders normal page if no articles but tag is supported" do
|
||||
Article.destroy_all
|
||||
expect { get "/t/#{tag.name}" }.not_to raise_error
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ RSpec.describe "StoriesIndex", type: :request do
|
|||
renders_proper_sidebar(navigation_link)
|
||||
end
|
||||
|
||||
it "doesn't render a featured scheduled article" do
|
||||
article = create(:article, featured: true, published_at: 1.hour.from_now)
|
||||
get "/"
|
||||
expect(response.body).not_to include(CGI.escapeHTML(article.title))
|
||||
end
|
||||
|
||||
def renders_proper_description
|
||||
expect(response.body).to include(Settings::Community.community_description)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe Admin::ChartsData, type: :service do
|
|||
end
|
||||
|
||||
it "returns proper previous period number" do
|
||||
create_list(:article, 2, published_at: 8.days.ago)
|
||||
create_list(:article, 2, :past, past_published_at: 8.days.ago)
|
||||
expect(described_class.new.call.first.third).to eq(2)
|
||||
end
|
||||
|
||||
|
|
@ -39,8 +39,7 @@ RSpec.describe Admin::ChartsData, type: :service do
|
|||
end
|
||||
|
||||
it "ignores today" do
|
||||
create(:article, published_at: Time.zone.today)
|
||||
|
||||
create(:article, :past, past_published_at: Time.zone.today)
|
||||
expect(described_class.new.call.first.second).to eq(0)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -67,5 +67,25 @@ RSpec.describe Articles::Attributes, type: :service do
|
|||
attrs = described_class.new({ title: "title" }, user).for_update(update_edited_at: false)
|
||||
expect(attrs[:edited_at]).to be_falsey
|
||||
end
|
||||
|
||||
it "sets published_at if update_published_at is true" do
|
||||
attrs = described_class.new({ title: "title" }, user).for_update(update_published_at: true)
|
||||
expect(attrs[:published_at]).to be_truthy
|
||||
end
|
||||
|
||||
it "doesn't set published_at if update_published_at is false" do
|
||||
attrs = described_class.new({ title: "title" }, user).for_update(update_published_at: false)
|
||||
expect(attrs[:published_at]).to be_falsey
|
||||
end
|
||||
|
||||
it "sets published_at correctly" do
|
||||
attrs = described_class.new({ title: "title", published_at: "2022-04-25" }, user).for_update
|
||||
expect(attrs[:published_at]).to eq(DateTime.new(2022, 4, 25))
|
||||
end
|
||||
|
||||
it "doesn't set published_at if it's nil and update_published_at is false" do
|
||||
attrs = described_class.new({ title: "title", published_at: nil }, user).for_update
|
||||
expect(attrs[:published_at]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -128,13 +128,9 @@ RSpec.describe Articles::Builder, type: :service do
|
|||
|
||||
context "when user_editor_v1" do
|
||||
let(:user) { create(:user) }
|
||||
let(:correct_attributes) do
|
||||
body = "---\ntitle: \npublished: false\ndescription: " \
|
||||
"\ntags: \n# cover_image: https://direct_url_to_image.jpg" \
|
||||
"\n# Use a ratio of 100:42 for best results.\n---\n\n"
|
||||
|
||||
let(:correct_attributes) do
|
||||
{
|
||||
body_markdown: body,
|
||||
processed_html: "",
|
||||
user_id: user.id
|
||||
}
|
||||
|
|
@ -142,10 +138,23 @@ RSpec.describe Articles::Builder, type: :service do
|
|||
|
||||
it "initializes an article with the correct attributes and does not need authorization" do
|
||||
user.setting.update(editor_version: "v1")
|
||||
allow(FeatureFlag).to receive(:enabled?).with(:schedule_articles).and_return(true)
|
||||
|
||||
subject, needs_authorization = described_class.call(user, tag, prefill)
|
||||
|
||||
expect(subject).to be_an_instance_of(Article)
|
||||
expect(subject).to have_attributes(correct_attributes)
|
||||
|
||||
date = Time.current.strftime("%Y-%m-%d")
|
||||
zone = Time.current.strftime("%z")
|
||||
|
||||
body_start = "---\ntitle: \npublished: false\ndescription: " \
|
||||
"\ntags: \n# cover_image: https://direct_url_to_image.jpg" \
|
||||
"\n# Use a ratio of 100:42 for best results.\n# published_at: #{date} "
|
||||
|
||||
expect(subject.body_markdown).to start_with(body_start)
|
||||
expect(subject.body_markdown).to end_with("#{zone}\n---\n\n")
|
||||
|
||||
expect(needs_authorization).to be false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,20 +19,6 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
expect(article).to be_persisted
|
||||
end
|
||||
|
||||
it "schedules a job" do
|
||||
valid_attributes[:published] = true
|
||||
sidekiq_assert_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
described_class.call(user, valid_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
it "delegates to the Mentions::CreateAll service" do
|
||||
valid_attributes[:published] = true
|
||||
allow(Mentions::CreateAll).to receive(:call)
|
||||
article = described_class.call(user, valid_attributes)
|
||||
expect(Mentions::CreateAll).to have_received(:call).with(article)
|
||||
end
|
||||
|
||||
it "creates a notification subscription" do
|
||||
expect do
|
||||
described_class.call(user, valid_attributes)
|
||||
|
|
@ -62,18 +48,6 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
expect(article.errors.size).to eq(1)
|
||||
end
|
||||
|
||||
it "doesn't schedule a job" do
|
||||
sidekiq_assert_no_enqueued_jobs only: Notifications::NotifiableActionWorker do
|
||||
described_class.call(user, invalid_body_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't delegate to the Mentions::CreateAll service" do
|
||||
allow(Mentions::CreateAll).to receive(:call)
|
||||
article = described_class.call(user, invalid_body_attributes)
|
||||
expect(Mentions::CreateAll).not_to have_received(:call).with(article)
|
||||
end
|
||||
|
||||
it "doesn't create a notification subscription" do
|
||||
expect do
|
||||
described_class.call(user, invalid_body_attributes)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ RSpec.describe Articles::Feeds::Basic, type: :service do
|
|||
let(:unique_tag_name) { "foo" }
|
||||
let!(:article) { create(:article, hotness_score: 10) }
|
||||
let!(:hot_story) do
|
||||
create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago, user_id: second_user.id)
|
||||
create(:article, :past, hotness_score: 1000, score: 1000, past_published_at: 3.hours.ago, user_id: second_user.id)
|
||||
end
|
||||
let!(:old_story) { create(:article, hotness_score: 500, published_at: 3.days.ago, tags: unique_tag_name) }
|
||||
let!(:old_story) { create(:article, :past, hotness_score: 500, past_published_at: 3.days.ago, tags: unique_tag_name) }
|
||||
let!(:low_scoring_article) { create(:article, score: -1000) }
|
||||
let!(:month_old_story) { create(:article, published_at: 1.month.ago) } # rubocop:disable RSpec/LetSetup
|
||||
let!(:month_old_story) { create(:article, :past, past_published_at: 1.month.ago) } # rubocop:disable RSpec/LetSetup
|
||||
|
||||
context "without a user" do
|
||||
let(:feed) { described_class.new(user: nil, number_of_articles: 100, page: 1) }
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ RSpec.describe Articles::Feeds::LargeForemExperimental, type: :service do
|
|||
let!(:feed) { described_class.new(user: user, number_of_articles: 100, page: 1) }
|
||||
let!(:article) { create(:article) }
|
||||
let!(:hot_story) do
|
||||
create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago, user_id: second_user.id)
|
||||
create(:article, :past, hotness_score: 1000, score: 1000, past_published_at: 3.hours.ago, user_id: second_user.id)
|
||||
end
|
||||
let!(:old_story) { create(:article, published_at: 3.days.ago) }
|
||||
let!(:old_story) { create(:article, :past, past_published_at: 3.days.ago) }
|
||||
let!(:low_scoring_article) { create(:article, score: -1000) }
|
||||
|
||||
describe "#featured_story_and_default_home_feed" do
|
||||
|
|
@ -179,7 +179,7 @@ RSpec.describe Articles::Feeds::LargeForemExperimental, type: :service do
|
|||
context "when no hot stories or recently published articles" do
|
||||
before do
|
||||
Article.delete_all
|
||||
create(:article, hotness_score: 0, score: 0, published_at: 3.days.ago)
|
||||
create(:article, :past, hotness_score: 0, score: 0, past_published_at: 3.days.ago)
|
||||
end
|
||||
|
||||
it "still returns articles" do
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe Articles::Feeds::Latest, type: :service do
|
||||
let!(:hot_article) do
|
||||
create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago, user: create(:user))
|
||||
create(:article, :past, hotness_score: 1000, score: 1000, past_published_at: 3.hours.ago, user: create(:user))
|
||||
end
|
||||
let!(:newest_article) { create(:article, published_at: 1.hour.ago) }
|
||||
let!(:month_old_article) { create(:article, published_at: 1.month.ago) }
|
||||
let!(:newest_article) { create(:article, :past, past_published_at: 1.hour.ago) }
|
||||
let!(:month_old_article) { create(:article, :past, past_published_at: 1.month.ago) }
|
||||
let!(:low_scoring_article) { create(:article, score: -1000) }
|
||||
|
||||
it "returns articles ordered by publishing date descending" do
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe Articles::Feeds::Timeframe, type: :service do
|
||||
let!(:hot_article) do
|
||||
create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago, user: create(:user))
|
||||
create(:article, :past, hotness_score: 1000, score: 1000, past_published_at: 3.hours.ago, user: create(:user))
|
||||
end
|
||||
let!(:low_scoring_article) { create(:article, score: -1000) }
|
||||
let!(:moderately_high_scoring_article) { create(:article, score: 20) }
|
||||
let!(:month_old_article) { create(:article, published_at: 1.month.ago) }
|
||||
let!(:month_old_article) { create(:article, :past, past_published_at: 1.month.ago) }
|
||||
|
||||
it "returns correct articles ordered by score", :aggregate_failures do
|
||||
result = described_class.call("week")
|
||||
|
|
|
|||
|
|
@ -52,25 +52,66 @@ RSpec.describe Articles::Updater, type: :service do
|
|||
context "when an article is updated and published the first time" do
|
||||
before { attributes[:published] = true }
|
||||
|
||||
it "enqueues a job to send a notification" do
|
||||
sidekiq_assert_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
described_class.call(user, draft, attributes)
|
||||
end
|
||||
# it "enqueues a job to send a notification" do
|
||||
# sidekiq_assert_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
# described_class.call(user, draft, attributes)
|
||||
# end
|
||||
# end
|
||||
|
||||
# it "delegates to the Mentions::CreateAll service to create mentions" do
|
||||
# allow(Mentions::CreateAll).to receive(:call)
|
||||
# described_class.call(user, draft, attributes)
|
||||
# expect(Mentions::CreateAll).to have_received(:call).with(draft)
|
||||
# end
|
||||
|
||||
# in case the article was published and unpublished before
|
||||
it "updates published_at to the current time when no published_at passed" do
|
||||
attributes[:published_at] = nil
|
||||
past_time = 1.year.ago
|
||||
draft.update_column(:published_at, past_time)
|
||||
described_class.call(user, draft, attributes)
|
||||
draft.reload
|
||||
expect(draft.published_at).to be_within(1.minute).of(Time.current)
|
||||
end
|
||||
|
||||
it "delegates to the Mentions::CreateAll service to create mentions" do
|
||||
allow(Mentions::CreateAll).to receive(:call)
|
||||
it "updates published_at to the current time when a past published_at is passed" do
|
||||
attributes[:published_at] = "2020-04-05 20:20"
|
||||
described_class.call(user, draft, attributes)
|
||||
expect(Mentions::CreateAll).to have_received(:call).with(draft)
|
||||
draft.reload
|
||||
expect(draft.published_at).to be_within(1.minute).of(Time.current)
|
||||
end
|
||||
|
||||
it "doesn't update published_at when a future published_at is passed" do
|
||||
attributes[:published_at] = 1.day.from_now
|
||||
described_class.call(user, draft, attributes)
|
||||
draft.reload
|
||||
expect(draft.published_at).to be_within(1.second).of(attributes[:published_at])
|
||||
end
|
||||
|
||||
it "doesn't update published_at when a past published_at is passed and an article was exported" do
|
||||
past = 10.days.ago
|
||||
draft.update_columns(published_at: past, published_from_feed: true)
|
||||
attributes[:published_at] = past
|
||||
described_class.call(user, draft, attributes)
|
||||
expect(draft.published_at).to be_within(1.second).of(past)
|
||||
end
|
||||
end
|
||||
|
||||
context "when an article is being updated and has already been published" do
|
||||
it "doesn't enqueue a job to send a notification" do
|
||||
# it "doesn't enqueue a job to send a notification" do
|
||||
# attributes[:published] = true
|
||||
# sidekiq_assert_not_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
# described_class.call(user, article, attributes)
|
||||
# end
|
||||
# end
|
||||
|
||||
it "doesn't update published_at" do
|
||||
attributes[:published] = true
|
||||
sidekiq_assert_not_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
described_class.call(user, article, attributes)
|
||||
end
|
||||
past_time = 1.year.ago
|
||||
article.update_column(:published_at, past_time)
|
||||
described_class.call(user, article, attributes)
|
||||
article.reload
|
||||
expect(article.published_at).to be_within(1.second).of(past_time)
|
||||
end
|
||||
|
||||
it "delegates to the Mentions::CreateAll service to create mentions" do
|
||||
|
|
@ -83,6 +124,14 @@ RSpec.describe Articles::Updater, type: :service do
|
|||
context "when an article is unpublished" do
|
||||
before { attributes[:published] = false }
|
||||
|
||||
it "doesn't update published_at" do
|
||||
published_at = 1.day.ago
|
||||
article.update_column(:published_at, published_at)
|
||||
described_class.call(user, article, attributes)
|
||||
article.reload
|
||||
expect(article.published_at).to be_within(1.second).of(published_at)
|
||||
end
|
||||
|
||||
it "doesn't send any notifications" do
|
||||
sidekiq_assert_not_enqueued_with(job: Notifications::NotifiableActionWorker) do
|
||||
described_class.call(user, article, attributes)
|
||||
|
|
@ -111,6 +160,15 @@ RSpec.describe Articles::Updater, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context "when an article is updated and remains unpublished" do
|
||||
it "doesn't update published_at" do
|
||||
attributes[:published] = false
|
||||
described_class.call(user, draft, attributes)
|
||||
article.reload
|
||||
expect(draft.published_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when an article is unpublished and contains comments" do
|
||||
let!(:comment) { create(:comment, user_id: user.id, commentable: article) }
|
||||
let(:notification) do
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ RSpec.describe Badges::AwardStreak, type: :service do
|
|||
|
||||
before do
|
||||
create(:badge, title: "4 Week Streak", slug: "4-week-streak")
|
||||
create(:article, user: user, published: true, published_at: 26.days.ago)
|
||||
create(:article, user: user, published: true, published_at: 19.days.ago)
|
||||
create(:article, user: user, published: true, published_at: 5.days.ago)
|
||||
create(:article, :past, user: user, published: true, past_published_at: 26.days.ago)
|
||||
create(:article, :past, user: user, published: true, past_published_at: 19.days.ago)
|
||||
create(:article, :past, user: user, published: true, past_published_at: 5.days.ago)
|
||||
end
|
||||
|
||||
it "awards badge to users with four straight weeks of articles" do
|
||||
create(:article, user: user, published: true, published_at: 12.days.ago)
|
||||
create(:article, :past, user: user, published: true, past_published_at: 12.days.ago)
|
||||
expect do
|
||||
described_class.call(weeks: 4)
|
||||
end.to change { user.reload.badges.size }.by(1)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ RSpec.describe Search::Article, type: :service do
|
|||
|
||||
it "supports sorting by published_at in ascending and descending order with a search term", :aggregate_failures do
|
||||
article1 = create(:article, tags: "ruby")
|
||||
article2 = create(:article, tags: "ruby", published_at: 1.week.ago)
|
||||
article2 = create(:article, :past, tags: "ruby", past_published_at: 1.week.ago)
|
||||
|
||||
results = described_class.search_documents(term: "ruby", sort_by: :published_at, sort_direction: :asc)
|
||||
expect(results.pluck(:id)).to eq([article2.id, article1.id])
|
||||
|
|
@ -137,7 +137,7 @@ RSpec.describe Search::Article, type: :service do
|
|||
it "supports sorting by published_at in ascending and descending order without a search term",
|
||||
:aggregate_failures do
|
||||
article1 = create(:article)
|
||||
article2 = create(:article, published_at: 1.week.ago)
|
||||
article2 = create(:article, :past, past_published_at: 1.week.ago)
|
||||
|
||||
results = described_class.search_documents(sort_by: :published_at, sort_direction: :asc)
|
||||
expect(results.pluck(:id)).to eq([article2.id, article1.id])
|
||||
|
|
@ -167,4 +167,5 @@ RSpec.describe Search::Article, type: :service do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Rails/PluckId
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ require "rails_helper"
|
|||
RSpec.describe Articles::Feeds::Basic, type: :system, js: true do
|
||||
let(:user) { create(:user) }
|
||||
let(:hot_story) do
|
||||
create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago)
|
||||
create(:article, :past, hotness_score: 1000, score: 1000, past_published_at: 3.hours.ago)
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -139,6 +139,31 @@ RSpec.describe "Views an article", type: :system do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when an article is scheduled" do
|
||||
let(:scheduled_article) { create(:article, user: user, published: true, published_at: Date.tomorrow) }
|
||||
let(:scheduled_article_path) { scheduled_article.path + query_params }
|
||||
let(:query_params) { "?preview=#{scheduled_article.password}" }
|
||||
|
||||
it "shows the article edit link for the author", js: true do
|
||||
visit scheduled_article_path
|
||||
edit_link = find("a#author-click-to-edit")
|
||||
expect(edit_link.matches_style?(display: "inline-block")).to be true
|
||||
end
|
||||
|
||||
it "doesn't show an article edit link for the non-authorized user" do
|
||||
sign_out user
|
||||
sign_in create(:user)
|
||||
visit scheduled_article_path
|
||||
expect(page.body).to include('display: none;">Click to edit</a>')
|
||||
end
|
||||
|
||||
it "doesn't show an article edit link when the user is not logged in" do
|
||||
sign_out user
|
||||
visit scheduled_article_path
|
||||
expect(page.body).not_to include("Click to edit")
|
||||
end
|
||||
end
|
||||
|
||||
describe "when an article is not published" do
|
||||
let(:article) { create(:article, user: article_user, published: false) }
|
||||
let(:article_path) { article.path + query_params }
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ RSpec.describe "User visits articles by tag", type: :system do
|
|||
let!(:func_tag) { create(:tag, name: "functional") }
|
||||
|
||||
let(:author) { create(:user, profile_image: nil) }
|
||||
let!(:article) { create(:article, tags: "javascript, IoT", user: author, published_at: 2.days.ago, score: 5) }
|
||||
let!(:article) do
|
||||
create(:article, :past, past_published_at: 2.days.ago, tags: "javascript, IoT", user: author, score: 5)
|
||||
end
|
||||
let!(:article2) { create(:article, tags: "functional", user: author, published_at: Time.current, score: 5) }
|
||||
let!(:article3) do
|
||||
create(:article, tags: "functional, javascript", user: author, published_at: 2.weeks.ago, score: 5)
|
||||
create(:article, :past, past_published_at: 2.weeks.ago, tags: "functional, javascript", user: author, score: 5)
|
||||
end
|
||||
|
||||
context "when user hasn't logged in" do
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ require "rails_helper"
|
|||
RSpec.describe "User visits articles by timeframe", type: :system do
|
||||
let(:author) { create(:user) }
|
||||
let!(:article) { create(:article, user: author, published_at: Time.current) }
|
||||
let!(:days_old_article) { create(:article, user: author, published_at: 2.days.ago) }
|
||||
let!(:weeks_old_article) { create(:article, user: author, published_at: 2.weeks.ago) }
|
||||
let!(:months_old_article) { create(:article, user: author, published_at: 2.months.ago) }
|
||||
let!(:years_old_article) { create(:article, user: author, published_at: 2.years.ago) }
|
||||
let!(:days_old_article) { create(:article, :past, past_published_at: 2.days.ago, user: author) }
|
||||
let!(:weeks_old_article) { create(:article, :past, past_published_at: 2.weeks.ago, user: author) }
|
||||
let!(:months_old_article) { create(:article, :past, past_published_at: 2.months.ago, user: author) }
|
||||
let!(:years_old_article) { create(:article, :past, past_published_at: 2.years.ago, user: author) }
|
||||
|
||||
def shows_correct_articles_count(count)
|
||||
expect(page).to have_selector(".crayons-story", visible: :visible, count: count)
|
||||
|
|
|
|||
104
spec/workers/articles/publish_worker_spec.rb
Normal file
104
spec/workers/articles/publish_worker_spec.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Articles::PublishWorker, type: :worker do
|
||||
include_examples "#enqueues_on_correct_queue", "medium_priority"
|
||||
|
||||
describe "#perform" do
|
||||
let(:worker) { subject }
|
||||
let!(:article) { create(:article, published: true) }
|
||||
|
||||
it "calls Slack::Messengers::ArticlePublished to send slack notifications" do
|
||||
allow(Slack::Messengers::ArticlePublished).to receive(:call)
|
||||
worker.perform
|
||||
expect(Slack::Messengers::ArticlePublished).to have_received(:call).with(article: article)
|
||||
end
|
||||
|
||||
it "sends notifications to mentioned users and followers" do
|
||||
allow(Notification).to receive(:send_to_mentioned_users_and_followers)
|
||||
worker.perform
|
||||
expect(Notification).to have_received(:send_to_mentioned_users_and_followers).with(article)
|
||||
end
|
||||
|
||||
it "schedules Notifications::NotifiableActionWorker" do
|
||||
args = [article.id, "Article", "Published"]
|
||||
sidekiq_assert_enqueued_with(job: Notifications::NotifiableActionWorker, args: args) do
|
||||
worker.perform
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't send notifications for an old article" do
|
||||
old_article = create(:article, :past, published: true, past_published_at: 1.year.ago)
|
||||
allow(Notification).to receive(:send_to_mentioned_users_and_followers)
|
||||
worker.perform
|
||||
expect(Notification).not_to have_received(:send_to_mentioned_users_and_followers).with(old_article)
|
||||
end
|
||||
|
||||
it "doesn't send notifications for a scheduled article" do
|
||||
scheduled_article = create(:article, published: true, published_at: 1.day.from_now)
|
||||
allow(Notification).to receive(:send_to_mentioned_users_and_followers)
|
||||
worker.perform
|
||||
expect(Notification).not_to have_received(:send_to_mentioned_users_and_followers).with(scheduled_article)
|
||||
end
|
||||
|
||||
context "with 2 articles" do
|
||||
let!(:article2) { create(:article, published: true) }
|
||||
|
||||
it "schedules Notifications::NotifiableActionWorker twice for 2 articles" do
|
||||
sidekiq_assert_enqueued_jobs(2, only: Notifications::NotifiableActionWorker) do
|
||||
worker.perform
|
||||
end
|
||||
end
|
||||
|
||||
it "sends notifications to mentioned users and followers for 2 articles" do
|
||||
allow(Notification).to receive(:send_to_mentioned_users_and_followers)
|
||||
worker.perform
|
||||
expect(Notification).to have_received(:send_to_mentioned_users_and_followers).with(article2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "creating notifications" do
|
||||
let!(:user2) { create(:user) }
|
||||
let(:article2) { create(:article, published: true, user: user2) }
|
||||
|
||||
before do
|
||||
user2.follow(article.user)
|
||||
end
|
||||
|
||||
it "creates a notification eventually" do
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs(only: Notifications::NotifiableActionWorker) do
|
||||
worker.perform
|
||||
end
|
||||
end.to change(Notification, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a context notification as well" do
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs(only: Notifications::NotifiableActionWorker) do
|
||||
worker.perform
|
||||
end
|
||||
end.to change(ContextNotification, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a notification for each article" do
|
||||
article2
|
||||
article.user.follow(article2.user)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs(only: Notifications::NotifiableActionWorker) do
|
||||
worker.perform
|
||||
end
|
||||
end.to change(Notification, :count).by(2)
|
||||
end
|
||||
|
||||
it "creates a ContextNotification for each article" do
|
||||
article2
|
||||
article.user.follow(article2.user)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs(only: Notifications::NotifiableActionWorker) do
|
||||
worker.perform
|
||||
end
|
||||
end.to change(ContextNotification, :count).by(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue