Skip to content
17 changes: 13 additions & 4 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2025-03-18 11:13:14 UTC using RuboCop version 1.74.0.
# on 2025-03-20 11:37:51 UTC using RuboCop version 1.74.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -45,27 +45,36 @@ RSpec/NamedSubject:
- 'spec/controllers/spree/admin/pages_controller_spec.rb'
- 'spec/lib/solidus_static_content/route_matcher_spec.rb'

# Offense count: 1
# Offense count: 2
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasAndBelongsToMany:
Exclude:
- 'app/models/spree/blog.rb'
- 'app/models/spree/page.rb'

# Offense count: 1
# Configuration parameters: IgnoreScopes, Include.
# Include: app/models/**/*.rb
Rails/InverseOf:
Exclude:
- 'app/models/spree/blog.rb'

# Offense count: 1
Rails/OutputSafety:
Exclude:
- 'app/helpers/spree/pages_helper.rb'

# Offense count: 4
# Offense count: 7
# Configuration parameters: ForbiddenMethods, AllowedMethods.
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
Rails/SkipsModelValidations:
Exclude:
- 'app/models/spree/blog.rb'
- 'db/migrate/20090625125735_extend_pages.rb'
- 'db/migrate/20090814113100_add_visible_to_pages.rb'

# Offense count: 1
# Offense count: 4
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Placeholder manifest file.
// the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js'
//= require 'spree/backend/views/blogs/taxonblog_autocomplete'
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
$.fn.taxonblogAutocomplete = function (options) {
'use strict';

var defaultOptions = {
multiple: true,
placeholder: Spree.translations.taxon_placeholder
};

options = $.extend({}, defaultOptions, options);

this.select2({
placeholder: options.placeholder,
multiple: options.multiple,
initSelection: function (element, callback) {
var ids = element.val();

if (options.multiple) {
var count = ids.split(",").length;

Spree.ajax({
type: "GET",
url: Spree.pathFor('api/taxons'),
data: {
ids: ids,
per_page: count,
without_children: true
},
success: function (data) {
callback(data['taxons']);
}
});
} else {

Spree.ajax({
type: "GET",
url: Spree.pathFor('api/taxons'),
data: {
ids: ids,
per_page: 1,
without_children: true
},
success: function (data) {
callback(data['taxons'][0]);
}
});
}
},
ajax: {
url: Spree.pathFor('api/taxons'),
datatype: 'json',
data: function (term, page) {
return {
per_page: 50,
page: page,
without_children: true,
q: {
name_cont: term
},
token: Spree.api_key
};
},
results: function (data, page) {
var more = page < data.pages;
return {
results: data['taxons'],
more: more
};
}
},
formatResult: function (taxon, container, query, escapeMarkup) {
return escapeMarkup(taxon.pretty_name);
},
formatSelection: function (taxon, container, escapeMarkup) {
return escapeMarkup(taxon.pretty_name);
}
});
};

Spree.ready(function () {
$('#blog_taxon_ids, .taxon_picker').taxonblogAutocomplete({
multiple: true,
});

$('#blog_primary_taxon_id').taxonblogAutocomplete({
multiple: false,
});
});
85 changes: 85 additions & 0 deletions app/models/spree/blog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

module Spree
class Blog < Spree::Base
AUTHOR_IMAGE_TYPE = 'AuthorImage'
ARTICLE_IMAGE_TYPE = 'ArticleImage'

extend FriendlyId
friendly_id :slug_candidates, use: :history

acts_as_list
has_and_belongs_to_many :stores, join_table: 'spree_blogs_stores'
has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: "Spree::Image"
has_many :tags, dependent: :delete_all, inverse_of: :blog
has_many :taxons, through: :tags, before_remove: :remove_taxon

belongs_to :primary_taxon, class_name: 'Spree::Taxon', optional: true

validates :title, :author, :content, :slug, presence: true
validates :slug, presence: true, uniqueness: { allow_blank: true, case_sensitive: true }

before_validation :normalize_slug, on: :update
after_destroy :punch_slug
after_touch :touch_taxons

scope :published, -> { where(status: :publish).where('publishing_date <= ?', Time.current) }
scope :by_store, ->(store) { joins(:stores).where("spree_blogs_stores.store_id = ?", store) }
scope :ordered_by_position, -> { order(:position) }

enum :status, { draft: 0, publish: 1 }

def author_image
images.find_by(image_type: AUTHOR_IMAGE_TYPE)
end

def article_images
images.where(image_type: ARTICLE_IMAGE_TYPE)
end

def attach_author_image(image)
images.create!(attachment: image, image_type: AUTHOR_IMAGE_TYPE)
end

def attach_article_images(attach_images)
attach_images.each do |image|
images.create!(attachment: image, image_type: ARTICLE_IMAGE_TYPE)
end
end

private

def normalize_slug
self.slug = normalize_friendly_id(slug)
end

def punch_slug
# punch slug with date prefix to allow reuse of original
update_column :slug, "#{Time.current.to_i}_#{slug}" unless frozen?
end

# Try building a slug based on the following fields in increasing order of specificity.
def slug_candidates
[
:title,
[:title, :author]
]
end

# Iterate through this product's taxons and taxonomies and touch their timestamps in a batch
def touch_taxons
taxons_to_touch = taxons.flat_map(&:self_and_ancestors).uniq
return if taxons_to_touch.empty?

Spree::Taxon.where(id: taxons_to_touch.map(&:id)).update_all(updated_at: Time.current)

taxonomy_ids_to_touch = taxons_to_touch.flat_map(&:taxonomy_id).uniq
Spree::Taxonomy.where(id: taxonomy_ids_to_touch).update_all(updated_at: Time.current)
end

def remove_taxon(taxon)
removed_tags = tags.where(taxon:)
removed_tags.each(&:remove_from_list)
end
end
end
13 changes: 13 additions & 0 deletions app/models/spree/tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Spree
class Tag < Spree::Base
self.table_name = 'spree_blogs_taxons'
acts_as_list scope: :taxon
belongs_to :blog, class_name: "Spree::Blog", inverse_of: :tags, touch: true, optional: true
belongs_to :taxon, class_name: "Spree::Taxon", inverse_of: :tags, touch: true, optional: true

# For https://github.com/spree/spree/issues/3494
validates :taxon_id, uniqueness: { scope: :blog_id, message: :already_linked }
end
end
11 changes: 11 additions & 0 deletions app/models/spree/taxon_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Spree
module TaxonDecorator
def self.prepended(base)
base.has_many :tags, -> { order(:position) }, dependent: :delete_all, inverse_of: :taxon
end
end
end

Spree::Taxon.prepend(Spree::TaxonDecorator)
18 changes: 17 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,32 @@ en:
many: Pages
one: Page
other: Pages
spree/blog:
few: Blogs
many: Blogs
one: Blog
other: Blogs
spree:
admin:
tab:
pages: Pages
blogs: Blogs
contents: Contents
back_to_static_pages_list: "Back to static pages list"
page: Page
content: Content
contents: Contents
new_blog: New Blog
blogs: Blogs
pages: Pages
stores: Stores
static_pages: Static pages
new_page: New page
new_page: New Page
editing_page: Editing page
link: Link
add_one: Create One
remove_image: Image Removed Successfully
edit_blog: Edit Blog
statuses:
draft: Draft
publish: Publish
16 changes: 16 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
post :update_positions
end
end

resources :blogs do
collection do
post :update_positions
end

member do
delete :delete_image
end
end
end

# Check if the layout is not the default layout to avoid conflicts with the starter_frontend routes
Expand All @@ -15,4 +25,10 @@
get '/(*path)', to: 'static_content#show', as: 'static'
end
end

if SolidusSupport.api_available?
namespace :api do
resources :blogs, only: [:index, :show, :create, :update, :destroy]
end
end
end
22 changes: 22 additions & 0 deletions db/migrate/20250318123655_create_blogs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class CreateBlogs < ActiveRecord::Migration[7.2]
def change
create_table :spree_blogs do |t|
t.integer :status, default: 0, null: false, comment: '0 is draft, 1 is publish'
t.string :slug
t.string :title
t.string :subtitle
t.string :author
t.datetime :publishing_date
t.text :content
t.integer :position, default: 0
t.references :primary_taxon, type: :integer, foreign_key: { to_table: :spree_taxons }

t.timestamps
end

add_index :spree_blogs, :slug, unique: true
add_index :spree_blogs, :position
end
end
17 changes: 17 additions & 0 deletions db/migrate/20250318125427_create_join_table_blogs_stores.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class CreateJoinTableBlogsStores < ActiveRecord::Migration[7.2]
def change
create_table :spree_blogs_stores, id: false do |t|
t.integer :blog_id, null: false
t.integer :store_id, null: false
t.timestamps
end

add_index :spree_blogs_stores, :blog_id
add_index :spree_blogs_stores, :store_id

add_foreign_key :spree_blogs_stores, :spree_blogs, column: :blog_id
add_foreign_key :spree_blogs_stores, :spree_stores, column: :store_id
end
end
7 changes: 7 additions & 0 deletions db/migrate/20250319092255_add_image_type_to_spree_assets.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddImageTypeToSpreeAssets < ActiveRecord::Migration[7.2]
def change
add_column :spree_assets, :image_type, :string
end
end
15 changes: 15 additions & 0 deletions db/migrate/20250320102130_create_spree_blogs_taxons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class CreateSpreeBlogsTaxons < ActiveRecord::Migration[7.2]
def change
create_table :spree_blogs_taxons do |t|
t.integer :blog_id
t.integer :taxon_id
t.integer :position
t.timestamps
end
add_index :spree_blogs_taxons, :blog_id
add_index :spree_blogs_taxons, :taxon_id
add_index :spree_blogs_taxons, :position
end
end
Loading