spree_core 0.70.RC1 → 0.70.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +10 -4
- data/app/assets/javascripts/admin/admin.js.erb +1 -1
- data/app/assets/javascripts/admin/orders/edit_form.js +3 -1
- data/app/assets/stylesheets/admin/admin-form.css.erb +1 -3
- data/app/assets/stylesheets/admin/admin-tables.css.erb +1 -1
- data/app/assets/stylesheets/admin/admin.css.erb +2 -2
- data/app/assets/stylesheets/admin/spree_core.css +0 -1
- data/app/controllers/admin/line_items_controller.rb +3 -1
- data/app/controllers/checkout_controller.rb +6 -0
- data/app/helpers/spree/base_helper.rb +21 -10
- data/app/models/creditcard.rb +15 -1
- data/app/models/inventory_unit.rb +6 -10
- data/app/models/line_item.rb +17 -2
- data/app/models/order.rb +5 -3
- data/app/models/product.rb +33 -21
- data/app/models/taxonomy.rb +1 -1
- data/app/views/admin/mail_methods/_form.html.erb +3 -3
- data/app/views/admin/orders/_line_item.html.erb +3 -1
- data/app/views/admin/orders/index.html.erb +3 -3
- data/app/views/admin/payments/source_views/_gateway.html.erb +1 -1
- data/app/views/admin/products/_form.html.erb +3 -3
- data/app/views/admin/products/new.html.erb +3 -3
- data/app/views/admin/shared/_report_criteria.html.erb +3 -3
- data/app/views/admin/variants/_form.html.erb +3 -3
- data/app/views/admin/variants/index.html.erb +1 -1
- data/app/views/layouts/admin.html.erb +1 -1
- data/app/views/order_mailer/confirm_email.text.erb +1 -1
- data/app/views/orders/_line_item.html.erb +7 -2
- data/config/initializers/rails_3_1.rb +12 -0
- data/config/locales/en.yml +2 -0
- data/lib/generators/spree/dummy/dummy_generator.rb +1 -0
- data/lib/generators/spree/site/site_generator.rb +7 -7
- data/lib/scopes/product.rb +6 -6
- data/lib/spree_base.rb +5 -6
- data/lib/spree_core/railtie.rb +1 -1
- data/lib/spree_core/version.rb +1 -1
- metadata +70 -68
- data/app/assets/stylesheets/admin/grids.css +0 -314
- data/config/initializers/disable_paperclip_log.rb +0 -2
data/README.md
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
Core
|
2
|
+
====
|
3
|
+
|
4
|
+
Core e-commerce functionality for the Spree project
|
5
|
+
|
6
|
+
|
1
7
|
Testing
|
2
|
-
|
8
|
+
-------
|
3
9
|
|
4
10
|
Create the test site
|
5
11
|
|
6
|
-
rake test_app
|
12
|
+
bundle exec rake test_app
|
7
13
|
|
8
14
|
Run the tests
|
9
15
|
|
10
|
-
rake spec
|
16
|
+
bundle exec rake spec
|
11
17
|
|
12
18
|
Run the coverage. After the rake task open coverage/index.html
|
13
19
|
|
14
|
-
rake rcov
|
20
|
+
bundle exec rake rcov
|
15
21
|
|
@@ -106,7 +106,7 @@ prep_product_autocomplete_data = function(data){
|
|
106
106
|
}
|
107
107
|
|
108
108
|
$.fn.product_autocomplete = function(){
|
109
|
-
$(this).autocomplete("/admin/products.json?authenticity_token=" +
|
109
|
+
$(this).autocomplete("/admin/products.json?authenticity_token=" + encodeURIComponent($('meta[name=csrf-token]').attr("content")), {
|
110
110
|
parse: prep_product_autocomplete_data,
|
111
111
|
formatItem: function(item) {
|
112
112
|
return format_product_autocomplete(item);
|
@@ -9,7 +9,9 @@ $.each($('td.qty input'), function(i, inpt){
|
|
9
9
|
type: "POST",
|
10
10
|
url: "/admin/orders/" + $('input#order_number').val() + "/line_items/" + $(id).val(),
|
11
11
|
data: ({_method: "put", "line_item[quantity]": $(this).val()}),
|
12
|
-
|
12
|
+
complete: function(resp){
|
13
|
+
$('#order-form-wrapper').html(resp.responseText);
|
14
|
+
}
|
13
15
|
});
|
14
16
|
|
15
17
|
}, 0,5);
|
@@ -53,9 +53,7 @@ fieldset#preferences textarea { height: 100px; }
|
|
53
53
|
|
54
54
|
|
55
55
|
.date-range-filter {width:220px;}
|
56
|
-
.date-range-filter input {width:
|
57
|
-
.date-range-filter .yui-u { width:50% }
|
58
|
-
|
56
|
+
.date-range-filter input {width:70px;}
|
59
57
|
|
60
58
|
/* Multi-column form layout
|
61
59
|
-------------------------------------------------------------- */
|
@@ -24,7 +24,7 @@ table.index th {
|
|
24
24
|
table.index tr.alt td {
|
25
25
|
background-color: #efefef; }
|
26
26
|
table.index.green th {
|
27
|
-
background: #cfefa7 url(<%= asset_path 'admin/grid_header_back_green.png' %>) top left repeat-x;
|
27
|
+
background: #cfefa7 url(<%= asset_path 'admin/bg/grid_header_back_green.png' %>) top left repeat-x;
|
28
28
|
border-left: 1px solid #E4FDB4;
|
29
29
|
border-top: 1px solid #E4FDB4;
|
30
30
|
border-right: 1px solid #B7CB90;
|
@@ -331,7 +331,7 @@ body {
|
|
331
331
|
.errorExplanation h2 {
|
332
332
|
font-size: 1.75em;
|
333
333
|
padding-left: 40px;
|
334
|
-
background: url(<%= asset_path 'admin/icons/
|
334
|
+
background: url(<%= asset_path 'admin/icons/32x32/3.png' %>) center left no-repeat; }
|
335
335
|
ul.checkbox-list {
|
336
336
|
list-style: none; }
|
337
337
|
ul.checkbox-list li {
|
@@ -406,7 +406,7 @@ body {
|
|
406
406
|
ul.taxonomy-actions li.disabled {
|
407
407
|
opacity: .5; }
|
408
408
|
h3.warning {
|
409
|
-
background-image: url(<%= asset_path 'admin/icons/
|
409
|
+
background-image: url(<%= asset_path 'admin/icons/exclamation.png' %>);
|
410
410
|
background-position: 10px 15px;
|
411
411
|
background-repeat: no-repeat;
|
412
412
|
padding-left: 35px; }
|
@@ -14,7 +14,9 @@ class Admin::LineItemsController < Admin::BaseController
|
|
14
14
|
format.html { render :partial => "admin/orders/form", :locals => {:order => @order.reload}, :layout => false }
|
15
15
|
end
|
16
16
|
else
|
17
|
-
|
17
|
+
respond_with(@line_item) do |format|
|
18
|
+
format.js { render :action => 'create', :locals => {:order => @order.reload}, :layout => false }
|
19
|
+
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
@@ -61,11 +61,17 @@ class CheckoutController < Spree::BaseController
|
|
61
61
|
def load_order
|
62
62
|
@order = current_order
|
63
63
|
redirect_to cart_path and return unless @order and @order.checkout_allowed?
|
64
|
+
raise_insufficient_quantity and return if @order.insufficient_stock_lines.present?
|
64
65
|
redirect_to cart_path and return if @order.completed?
|
65
66
|
@order.state = params[:state] if params[:state]
|
66
67
|
state_callback(:before)
|
67
68
|
end
|
68
69
|
|
70
|
+
def raise_insufficient_quantity
|
71
|
+
flash[:error] = t('spree_inventory_error_flash_for_insufficient_quantity')
|
72
|
+
redirect_to cart_path
|
73
|
+
end
|
74
|
+
|
69
75
|
def state_callback(before_or_after = :before)
|
70
76
|
method_name = :"#{before_or_after}_#{@order.state}"
|
71
77
|
send(method_name) if respond_to?(method_name, true)
|
@@ -35,8 +35,19 @@ module Spree::BaseHelper
|
|
35
35
|
|
36
36
|
# human readable list of variant options
|
37
37
|
def variant_options(v, allow_back_orders = Spree::Config[:allow_backorders], include_style = true)
|
38
|
+
ActiveSupport::Deprecation.warn("variant_options method is deprecated, and will be removed in 0.80.0", caller)
|
38
39
|
list = v.options_text
|
39
|
-
|
40
|
+
|
41
|
+
# We shouldn't show out of stock if the product is infact in stock
|
42
|
+
# or when we're not allowing backorders.
|
43
|
+
unless (allow_back_orders || v.in_stock?)
|
44
|
+
list = if include_style
|
45
|
+
content_tag(:span, "(#{t(:out_of_stock)}) #{list}", :class => "out-of-stock")
|
46
|
+
else
|
47
|
+
"#{t(:out_of_stock)} #{list}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
40
51
|
list
|
41
52
|
end
|
42
53
|
|
@@ -56,12 +67,12 @@ module Spree::BaseHelper
|
|
56
67
|
def meta_data_tags
|
57
68
|
object = instance_variable_get('@'+controller_name.singularize)
|
58
69
|
meta = { :keywords => Spree::Config[:default_meta_keywords], :description => Spree::Config[:default_meta_description] }
|
59
|
-
|
70
|
+
|
60
71
|
if object.kind_of?(ActiveRecord::Base)
|
61
72
|
meta[:keywords] = object.meta_keywords if object[:meta_keywords].present?
|
62
73
|
meta[:description] = object.meta_description if object[:meta_description].present?
|
63
74
|
end
|
64
|
-
|
75
|
+
|
65
76
|
meta.map do |name, content|
|
66
77
|
tag('meta', :name => name, :content => content)
|
67
78
|
end.join("\n")
|
@@ -88,7 +99,7 @@ module Spree::BaseHelper
|
|
88
99
|
def logo(image_path=Spree::Config[:logo])
|
89
100
|
link_to image_tag(image_path), root_path
|
90
101
|
end
|
91
|
-
|
102
|
+
|
92
103
|
def flash_messages
|
93
104
|
[:notice, :error].map do |msg_type|
|
94
105
|
if flash[msg_type]
|
@@ -98,7 +109,7 @@ module Spree::BaseHelper
|
|
98
109
|
end
|
99
110
|
end.join("\n").html_safe
|
100
111
|
end
|
101
|
-
|
112
|
+
|
102
113
|
def breadcrumbs(taxon, separator=" » ")
|
103
114
|
return "" if current_page?("/") || taxon.nil?
|
104
115
|
separator = raw(separator)
|
@@ -120,8 +131,8 @@ module Spree::BaseHelper
|
|
120
131
|
root_taxon.children.map do |taxon|
|
121
132
|
css_class = (current_taxon && current_taxon.self_and_ancestors.include?(taxon)) ? 'current' : nil
|
122
133
|
content_tag :li, :class => css_class do
|
123
|
-
link_to(taxon.name, seo_url(taxon)) +
|
124
|
-
taxons_tree(taxon, current_taxon, max_level - 1)
|
134
|
+
link_to(taxon.name, seo_url(taxon)) +
|
135
|
+
taxons_tree(taxon, current_taxon, max_level - 1)
|
125
136
|
end
|
126
137
|
end.join("\n").html_safe
|
127
138
|
end
|
@@ -131,7 +142,7 @@ module Spree::BaseHelper
|
|
131
142
|
return Country.all unless zone = Zone.find_by_name(Spree::Config[:checkout_zone])
|
132
143
|
zone.country_list
|
133
144
|
end
|
134
|
-
|
145
|
+
|
135
146
|
def format_price(price, options={})
|
136
147
|
options.assert_valid_keys(:show_vat_text)
|
137
148
|
options.reverse_merge! :show_vat_text => Spree::Config[:show_price_inc_vat]
|
@@ -142,7 +153,7 @@ module Spree::BaseHelper
|
|
142
153
|
formatted_price
|
143
154
|
end
|
144
155
|
end
|
145
|
-
|
156
|
+
|
146
157
|
# generates nested url to product based on supplied taxon
|
147
158
|
def seo_url(taxon, product = nil)
|
148
159
|
return '/t/' + taxon.permalink if product.nil?
|
@@ -150,7 +161,7 @@ module Spree::BaseHelper
|
|
150
161
|
"not used anymore. Use product_url instead. (called from #{caller[0]})"
|
151
162
|
return product_url(product)
|
152
163
|
end
|
153
|
-
|
164
|
+
|
154
165
|
def current_orders_product_count
|
155
166
|
if current_order.blank? || current_order.item_count < 1
|
156
167
|
return 0
|
data/app/models/creditcard.rb
CHANGED
@@ -2,6 +2,7 @@ class Creditcard < ActiveRecord::Base
|
|
2
2
|
has_many :payments, :as => :source
|
3
3
|
|
4
4
|
before_save :set_last_digits
|
5
|
+
after_validation :set_card_type
|
5
6
|
|
6
7
|
attr_accessor :number, :verification_value
|
7
8
|
|
@@ -23,6 +24,19 @@ class Creditcard < ActiveRecord::Base
|
|
23
24
|
self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
|
24
25
|
end
|
25
26
|
|
27
|
+
# cheap hack to get to the type? method from deep within ActiveMerchant without stomping on
|
28
|
+
# potentially existing methods in CreditCard
|
29
|
+
class CardDetector
|
30
|
+
class << self
|
31
|
+
include ActiveMerchant::Billing::CreditCardMethods::ClassMethods
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# sets self.cc_type while we still have the card number
|
36
|
+
def set_card_type
|
37
|
+
self.cc_type ||= CardDetector.type?(self.number)
|
38
|
+
end
|
39
|
+
|
26
40
|
def name?
|
27
41
|
first_name? && last_name?
|
28
42
|
end
|
@@ -243,7 +257,7 @@ class Creditcard < ActiveRecord::Base
|
|
243
257
|
|
244
258
|
def spree_cc_type
|
245
259
|
return "visa" if ENV['RAILS_ENV'] == "development"
|
246
|
-
self.
|
260
|
+
self.cc_type
|
247
261
|
end
|
248
262
|
|
249
263
|
# Saftey check to make sure we're not accidentally performing operations on a live gateway.
|
@@ -22,12 +22,6 @@ class InventoryUnit < ActiveRecord::Base
|
|
22
22
|
after_transition :to => 'returned', :do => :restock_variant
|
23
23
|
end
|
24
24
|
|
25
|
-
# method deprecated in favour of adjust_units (which creates & destroys units as needed).
|
26
|
-
def self.sell_units(order)
|
27
|
-
warn "[DEPRECATION] `InventoryUnits#sell_units` is deprecated. Please use `InventoryUnits#assign_opening_inventory` instead. (called from #{caller[0]})"
|
28
|
-
self.adjust_units(order)
|
29
|
-
end
|
30
|
-
|
31
25
|
# Assigns inventory to a newly completed order.
|
32
26
|
# Should only be called once during the life-cycle of an order, on transition to completed.
|
33
27
|
#
|
@@ -88,7 +82,10 @@ class InventoryUnit < ActiveRecord::Base
|
|
88
82
|
end
|
89
83
|
|
90
84
|
def self.destroy_units(order, variant, quantity)
|
91
|
-
variant_units = order.inventory_units.group_by(&:variant_id)
|
85
|
+
variant_units = order.inventory_units.group_by(&:variant_id)
|
86
|
+
return unless variant_units.include? variant.id
|
87
|
+
|
88
|
+
variant_units = variant_units[variant.id].sort_by(&:state)
|
92
89
|
|
93
90
|
quantity.times do
|
94
91
|
inventory_unit = variant_units.shift
|
@@ -97,9 +94,8 @@ class InventoryUnit < ActiveRecord::Base
|
|
97
94
|
end
|
98
95
|
|
99
96
|
def self.create_units(order, variant, sold, back_order)
|
100
|
-
if back_order > 0 && !Spree::Config[:allow_backorders]
|
101
|
-
|
102
|
-
end
|
97
|
+
return if back_order > 0 && !Spree::Config[:allow_backorders]
|
98
|
+
|
103
99
|
|
104
100
|
shipment = order.shipments.detect {|shipment| !shipment.shipped? }
|
105
101
|
|
data/app/models/line_item.rb
CHANGED
@@ -10,6 +10,8 @@ class LineItem < ActiveRecord::Base
|
|
10
10
|
validates :variant, :presence => true
|
11
11
|
validates :quantity, :numericality => { :only_integer => true, :message => I18n.t("validation.must_be_int") }
|
12
12
|
validates :price, :numericality => true
|
13
|
+
validate :stock_availability
|
14
|
+
|
13
15
|
# validate :meta_validation_of_quantities
|
14
16
|
|
15
17
|
attr_accessible :quantity
|
@@ -58,6 +60,14 @@ class LineItem < ActiveRecord::Base
|
|
58
60
|
self.quantity = 0 if self.quantity.nil? || self.quantity < 0
|
59
61
|
end
|
60
62
|
|
63
|
+
def sufficient_stock?
|
64
|
+
Spree::Config[:allow_backorders] ? true : (self.variant.on_hand >= self.quantity)
|
65
|
+
end
|
66
|
+
|
67
|
+
def insufficient_stock?
|
68
|
+
!sufficient_stock?
|
69
|
+
end
|
70
|
+
|
61
71
|
private
|
62
72
|
def update_inventory
|
63
73
|
return true unless self.order.completed?
|
@@ -89,7 +99,12 @@ class LineItem < ActiveRecord::Base
|
|
89
99
|
if order.try(:inventory_units).to_a.any?{|unit| unit.variant_id == variant_id && unit.shipped?}
|
90
100
|
errors.add :base, I18n.t("cannot_destory_line_item_as_inventory_units_have_shipped")
|
91
101
|
return false
|
92
|
-
|
93
|
-
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def stock_availability
|
106
|
+
return if sufficient_stock?
|
107
|
+
errors.add(:quantiy, "can't be greater than avaiable stock.")
|
108
|
+
end
|
94
109
|
end
|
95
110
|
|
data/app/models/order.rb
CHANGED
@@ -45,8 +45,6 @@ class Order < ActiveRecord::Base
|
|
45
45
|
|
46
46
|
make_permalink :field => :number
|
47
47
|
|
48
|
-
attr_accessor :out_of_stock_items
|
49
|
-
|
50
48
|
class_attribute :update_hooks
|
51
49
|
self.update_hooks = Set.new
|
52
50
|
|
@@ -318,7 +316,7 @@ class Order < ActiveRecord::Base
|
|
318
316
|
# Called after transition to complete state when payments will have been processed
|
319
317
|
def finalize!
|
320
318
|
update_attribute(:completed_at, Time.now)
|
321
|
-
|
319
|
+
InventoryUnit.assign_opening_inventory(self)
|
322
320
|
# lock any optional adjustments (coupon promotions, etc.)
|
323
321
|
adjustments.optional.each { |adjustment| adjustment.update_attribute("locked", true) }
|
324
322
|
OrderMailer.confirm_email(self).deliver
|
@@ -378,6 +376,10 @@ class Order < ActiveRecord::Base
|
|
378
376
|
line_items.map{|li| li.variant.product}
|
379
377
|
end
|
380
378
|
|
379
|
+
def insufficient_stock_lines
|
380
|
+
line_items.select &:insufficient_stock?
|
381
|
+
end
|
382
|
+
|
381
383
|
private
|
382
384
|
def create_user
|
383
385
|
self.email = user.email if self.user and not user.anonymous?
|
data/app/models/product.rb
CHANGED
@@ -44,18 +44,17 @@ class Product < ActiveRecord::Base
|
|
44
44
|
after_save :save_master
|
45
45
|
|
46
46
|
has_many :variants,
|
47
|
-
:conditions => ["
|
48
|
-
:order =>
|
49
|
-
|
47
|
+
:conditions => ["#{Variant.table_name}.is_master = ? AND #{Variant.table_name}.deleted_at IS NULL", false],
|
48
|
+
:order => "#{Variant.table_name}.position ASC"
|
50
49
|
|
51
50
|
has_many :variants_including_master,
|
52
51
|
:class_name => 'Variant',
|
53
|
-
:conditions => ["
|
52
|
+
:conditions => ["#{Variant.table_name}.deleted_at IS NULL"],
|
54
53
|
:dependent => :destroy
|
55
54
|
|
56
55
|
has_many :variants_with_only_master,
|
57
56
|
:class_name => 'Variant',
|
58
|
-
:conditions => ["
|
57
|
+
:conditions => ["#{Variant.table_name}.deleted_at IS NULL AND #{Variant.table_name}.is_master = ?", true],
|
59
58
|
:dependent => :destroy
|
60
59
|
|
61
60
|
|
@@ -74,30 +73,43 @@ class Product < ActiveRecord::Base
|
|
74
73
|
|
75
74
|
include ::Scopes::Product
|
76
75
|
|
77
|
-
#RAILS3 TODO - scopes are duplicated here and in
|
76
|
+
#RAILS3 TODO - scopes are duplicated here and in scopes/product.rb - can we DRY it up?
|
78
77
|
# default product scope only lists available and non-deleted products
|
79
|
-
|
78
|
+
class << self
|
79
|
+
def not_deleted
|
80
|
+
where(Product.arel_table[:deleted_at].eq(nil))
|
81
|
+
end
|
82
|
+
|
83
|
+
def available(available_on = nil)
|
84
|
+
where(Product.arel_table[:available_on].lteq(available_on || Time.zone.now ))
|
85
|
+
end
|
80
86
|
|
81
|
-
|
87
|
+
#RAILS 3 TODO - this scope doesn't match the original 2.3.x version, needs attention (but it works)
|
88
|
+
def active
|
89
|
+
not_deleted.available
|
90
|
+
end
|
82
91
|
|
83
|
-
|
84
|
-
|
92
|
+
def on_hand
|
93
|
+
where(Product.arel_table[:count_on_hand].gteq(0))
|
94
|
+
end
|
85
95
|
|
86
|
-
|
96
|
+
def id_equals(input_id)
|
97
|
+
where(Product.arel_table[:id].eq(input_id))
|
98
|
+
end
|
87
99
|
|
100
|
+
def taxons_name_eq(name)
|
101
|
+
joins(:taxons).where(Taxon.arel_table[:name].eq(name))
|
102
|
+
end
|
103
|
+
end
|
88
104
|
if (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL')
|
89
|
-
if
|
90
|
-
scope :group_by_products_id, { :group => Product.column_names.map{|col_name| "
|
105
|
+
if Product.table_exists?
|
106
|
+
scope :group_by_products_id, { :group => Product.column_names.map{|col_name| "#{Product.table_name}.#{col_name}"} }
|
91
107
|
end
|
92
108
|
else
|
93
|
-
scope :group_by_products_id, { :group => "
|
109
|
+
scope :group_by_products_id, { :group => "#{Product.table_name}.id" }
|
94
110
|
end
|
95
111
|
search_methods :group_by_products_id
|
96
112
|
|
97
|
-
scope :id_equals, lambda { |input_id| where("products.id = ?", input_id) }
|
98
|
-
|
99
|
-
scope :taxons_name_eq, lambda { |name| joins(:taxons).where("taxons.name = ?", name) }
|
100
|
-
|
101
113
|
# ----------------------------------------------------------------------------------------------------------
|
102
114
|
#
|
103
115
|
# The following methods are deprecated and will be removed in a future version of Spree
|
@@ -135,7 +147,7 @@ class Product < ActiveRecord::Base
|
|
135
147
|
|
136
148
|
# returns true if the product has any variants (the master variant is not a member of the variants array)
|
137
149
|
def has_variants?
|
138
|
-
|
150
|
+
variants.any?
|
139
151
|
end
|
140
152
|
|
141
153
|
# returns the number of inventory units "on_hand" for this product
|
@@ -151,7 +163,7 @@ class Product < ActiveRecord::Base
|
|
151
163
|
|
152
164
|
# Returns true if there are inventory units (any variant) with "on_hand" state for this product
|
153
165
|
def has_stock?
|
154
|
-
master.in_stock? ||
|
166
|
+
master.in_stock? || variants.any?(&:in_stock?)
|
155
167
|
end
|
156
168
|
|
157
169
|
def tax_category
|
@@ -236,7 +248,7 @@ class Product < ActiveRecord::Base
|
|
236
248
|
end
|
237
249
|
|
238
250
|
private
|
239
|
-
|
251
|
+
|
240
252
|
def sanitize_permalink
|
241
253
|
self.permalink = self.permalink.to_url
|
242
254
|
end
|