shoperb-theme-editor 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CONTRIBUTING.md +40 -0
- data/LICENSE.md +21 -0
- data/README.md +124 -0
- data/bin/shoperb +313 -0
- data/lib/shoperb_theme_editor/api/server.rb +10 -0
- data/lib/shoperb_theme_editor/api/views/callback.erb +53 -0
- data/lib/shoperb_theme_editor/api.rb +264 -0
- data/lib/shoperb_theme_editor/build/json.rb +167 -0
- data/lib/shoperb_theme_editor/build/liquid.rb +38 -0
- data/lib/shoperb_theme_editor/build/section.rb +51 -0
- data/lib/shoperb_theme_editor/build/settings.rb +224 -0
- data/lib/shoperb_theme_editor/build.rb +68 -0
- data/lib/shoperb_theme_editor/configuration.rb +98 -0
- data/lib/shoperb_theme_editor/error.rb +35 -0
- data/lib/shoperb_theme_editor/ext/array.rb +17 -0
- data/lib/shoperb_theme_editor/ext/nil_class.rb +7 -0
- data/lib/shoperb_theme_editor/ext/sequel.rb +82 -0
- data/lib/shoperb_theme_editor/ext.rb +2 -0
- data/lib/shoperb_theme_editor/init.rb +87 -0
- data/lib/shoperb_theme_editor/logger.rb +58 -0
- data/lib/shoperb_theme_editor/mounter/models/address.rb +65 -0
- data/lib/shoperb_theme_editor/mounter/models/attribute.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/attribute_key.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/base.rb +196 -0
- data/lib/shoperb_theme_editor/mounter/models/blog_category.rb +47 -0
- data/lib/shoperb_theme_editor/mounter/models/blog_post.rb +45 -0
- data/lib/shoperb_theme_editor/mounter/models/brand.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/cart.rb +35 -0
- data/lib/shoperb_theme_editor/mounter/models/cart_item.rb +71 -0
- data/lib/shoperb_theme_editor/mounter/models/category.rb +99 -0
- data/lib/shoperb_theme_editor/mounter/models/collection.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/country.rb +18 -0
- data/lib/shoperb_theme_editor/mounter/models/currency.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/custom_field.rb +22 -0
- data/lib/shoperb_theme_editor/mounter/models/customer.rb +77 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_customer_group.rb +12 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_group.rb +13 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_subscription.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/customer_subscription_plan.rb +32 -0
- data/lib/shoperb_theme_editor/mounter/models/discount.rb +40 -0
- data/lib/shoperb_theme_editor/mounter/models/discount_variant.rb +15 -0
- data/lib/shoperb_theme_editor/mounter/models/image.rb +51 -0
- data/lib/shoperb_theme_editor/mounter/models/language.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/link.rb +61 -0
- data/lib/shoperb_theme_editor/mounter/models/media_file.rb +19 -0
- data/lib/shoperb_theme_editor/mounter/models/menu.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/meta.rb +10 -0
- data/lib/shoperb_theme_editor/mounter/models/news_item.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/order.rb +133 -0
- data/lib/shoperb_theme_editor/mounter/models/order_item.rb +137 -0
- data/lib/shoperb_theme_editor/mounter/models/order_item_attribute.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return.rb +42 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_item.rb +29 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_item_entity.rb +25 -0
- data/lib/shoperb_theme_editor/mounter/models/order_return_parcel.rb +20 -0
- data/lib/shoperb_theme_editor/mounter/models/page.rb +26 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_card.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_method.rb +67 -0
- data/lib/shoperb_theme_editor/mounter/models/payment_provider.rb +23 -0
- data/lib/shoperb_theme_editor/mounter/models/product.rb +144 -0
- data/lib/shoperb_theme_editor/mounter/models/product_attribute.rb +32 -0
- data/lib/shoperb_theme_editor/mounter/models/product_search.rb +53 -0
- data/lib/shoperb_theme_editor/mounter/models/product_type.rb +21 -0
- data/lib/shoperb_theme_editor/mounter/models/review.rb +38 -0
- data/lib/shoperb_theme_editor/mounter/models/search.rb +11 -0
- data/lib/shoperb_theme_editor/mounter/models/shipping_method.rb +39 -0
- data/lib/shoperb_theme_editor/mounter/models/shop.rb +58 -0
- data/lib/shoperb_theme_editor/mounter/models/state.rb +17 -0
- data/lib/shoperb_theme_editor/mounter/models/theme.rb +89 -0
- data/lib/shoperb_theme_editor/mounter/models/variant.rb +96 -0
- data/lib/shoperb_theme_editor/mounter/models/variant_attribute.rb +46 -0
- data/lib/shoperb_theme_editor/mounter/models/vendor.rb +38 -0
- data/lib/shoperb_theme_editor/mounter/server/assets.rb +35 -0
- data/lib/shoperb_theme_editor/mounter/server/defaults.rb +44 -0
- data/lib/shoperb_theme_editor/mounter/server/exception_handler.rb +22 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_footer.liquid +0 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_header.liquid +0 -0
- data/lib/shoperb_theme_editor/mounter/server/partials/_shoperb_stylesheets.liquid +3 -0
- data/lib/shoperb_theme_editor/mounter/server/renderer.rb +166 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/cart.rb +127 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/dummy.rb +34 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/locale.rb +31 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/pages.rb +33 -0
- data/lib/shoperb_theme_editor/mounter/server/routes/search.rb +18 -0
- data/lib/shoperb_theme_editor/mounter/server/routes.rb +366 -0
- data/lib/shoperb_theme_editor/mounter/server/routes_helper.rb +278 -0
- data/lib/shoperb_theme_editor/mounter/server.rb +66 -0
- data/lib/shoperb_theme_editor/mounter.rb +30 -0
- data/lib/shoperb_theme_editor/os.rb +13 -0
- data/lib/shoperb_theme_editor/package.rb +81 -0
- data/lib/shoperb_theme_editor/sync/images.rb +69 -0
- data/lib/shoperb_theme_editor/sync/pagination.rb +52 -0
- data/lib/shoperb_theme_editor/sync.rb +229 -0
- data/lib/shoperb_theme_editor/translations.rb +22 -0
- data/lib/shoperb_theme_editor/utils.rb +50 -0
- data/lib/shoperb_theme_editor.rb +159 -0
- data/shoperb_theme_editor.gemspec +60 -0
- metadata +510 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module Shoperb module Theme module Editor
|
2
|
+
module Mounter
|
3
|
+
|
4
|
+
Editor.autoload_all self, "mounter"
|
5
|
+
|
6
|
+
module Model
|
7
|
+
|
8
|
+
def self.console
|
9
|
+
binding.pry
|
10
|
+
end
|
11
|
+
|
12
|
+
Editor.autoload_all self, "mounter/models"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.start
|
16
|
+
Rack::Handler::WEBrick.run(Server.new,
|
17
|
+
Port: Editor["port"],
|
18
|
+
AccessLog: [],
|
19
|
+
Logger: WEBrick::Log::new(Os["/dev/null"], 7),
|
20
|
+
StartCallback: -> {
|
21
|
+
Logger.success "Server started\nBrowse http://0.0.0.0:#{Editor["port"]}\n"
|
22
|
+
}
|
23
|
+
)
|
24
|
+
rescue Errno::EADDRINUSE => e
|
25
|
+
Logger.error e.message
|
26
|
+
exit!
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end end end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'artisans'
|
2
|
+
|
3
|
+
module Shoperb module Theme module Editor
|
4
|
+
module Package
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def unzip file
|
9
|
+
Zip::File.open(file.path) { |zip_file|
|
10
|
+
spec = zip_file.glob("*/config/spec.json").first
|
11
|
+
if spec && (spec_content = zip_file.read(spec).presence)
|
12
|
+
zip_handle = Editor.handle(spec_content)
|
13
|
+
else
|
14
|
+
zip_handle = Editor["handle"] = zip_file.entries.first.name.split("/").first
|
15
|
+
end
|
16
|
+
|
17
|
+
# remove old files, which are not present in theme zip anymore
|
18
|
+
# but only from folders, mentioned in zip
|
19
|
+
zip_files = zip_file.entries.map{|f| f.to_s.gsub(/^#{zip_handle}\//, '') }
|
20
|
+
zip_folders = zip_files.map{|f| f.split('/')[0] }.uniq
|
21
|
+
files_to_remove = Dir.glob("{#{zip_folders.join(',')}}/**/*.*") - zip_files
|
22
|
+
|
23
|
+
zip_file.each do |entry|
|
24
|
+
entry_name = Pathname.new(entry.name).cleanpath.to_s
|
25
|
+
name = entry_name.gsub(/\A#{zip_handle}\//, "")
|
26
|
+
extract_path = Utils.base + name
|
27
|
+
FileUtils.mkdir_p extract_path.dirname
|
28
|
+
Logger.notify "Extracting #{name}" do
|
29
|
+
entry.extract(extract_path) { true }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
files_to_remove.each { |f| File.delete(f) }
|
33
|
+
}
|
34
|
+
ensure
|
35
|
+
Utils.rm_tempfile file
|
36
|
+
end
|
37
|
+
|
38
|
+
def zip
|
39
|
+
compiler = Editor.compiler("/system/assets/#{Editor["oauth-site"]}/#{Editor.handle}/", digests: true)
|
40
|
+
|
41
|
+
zip = Zip::OutputStream.write_buffer do |out|
|
42
|
+
compiler.compiled_files do |path, content, type = :file|
|
43
|
+
case type
|
44
|
+
when :symlink
|
45
|
+
write_symlink(out, path, content)
|
46
|
+
else
|
47
|
+
write_file(out, path) { content }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
Utils.write_file("debug.zip") { zip.string } if Editor["verbose"]
|
52
|
+
Utils.mk_tempfile zip.string, "#{handle.basename}-", ".zip"
|
53
|
+
end
|
54
|
+
|
55
|
+
def write_file out, file
|
56
|
+
out.put_next_entry(zip_file_path = handle + file)
|
57
|
+
content = block_given? ? yield : file.read
|
58
|
+
if file.to_s[0..6] == "sources"
|
59
|
+
out.write content
|
60
|
+
else
|
61
|
+
Logger.notify "Packing #{file}" do
|
62
|
+
out.write content
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_symlink out, current, target
|
68
|
+
entry = out.put_next_entry(zip_file_path = handle + current)
|
69
|
+
entry.instance_variable_set("@ftype", :symlink)
|
70
|
+
entry.instance_variable_set("@filepath", target.to_s)
|
71
|
+
Logger.notify "Packing #{current}" do
|
72
|
+
out.write((handle + target).relative_path_from(handle + current).to_s[3..-1])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle
|
77
|
+
Pathname.new(Editor.handle)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end end end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Shoperb module Theme module Editor
|
2
|
+
module Sync
|
3
|
+
module Images
|
4
|
+
|
5
|
+
extend self
|
6
|
+
mattr_accessor :queue
|
7
|
+
self.queue = []
|
8
|
+
|
9
|
+
mattr_accessor :dir
|
10
|
+
self.dir = Utils.base + "data" + "assets" + "images"
|
11
|
+
|
12
|
+
def process
|
13
|
+
FileUtils.mkdir_p(dir)
|
14
|
+
Sync.process Mounter::Model::Image do |image|
|
15
|
+
if image["entity_id"]
|
16
|
+
process_sizes(image)
|
17
|
+
image
|
18
|
+
end
|
19
|
+
end
|
20
|
+
threaded_download "Downloading images"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def process_sizes image
|
26
|
+
image["sizes"] = image["sizes"].select{|size,url|url}.map do |size, url|
|
27
|
+
filename = filename_from_url(url)
|
28
|
+
enqueue_download(url, filename)
|
29
|
+
[size, filename.relative_path_from(dir).to_s]
|
30
|
+
end.compact.to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
def filename_from_url url
|
34
|
+
dir + "#{Pathname.new(url).basename.to_s.split("?")[0]}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def enqueue_download url, filename
|
38
|
+
(queue << -> {
|
39
|
+
begin
|
40
|
+
Utils.write_file(filename) do
|
41
|
+
begin
|
42
|
+
Timeout::timeout(10) do
|
43
|
+
URI(url).open.read
|
44
|
+
end
|
45
|
+
rescue Timeout::Error => e
|
46
|
+
retry
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue Exception => e
|
50
|
+
Logger.error("#{e.message} (#{url} => #{filename})")
|
51
|
+
end
|
52
|
+
}) unless File.exist?(filename)
|
53
|
+
end
|
54
|
+
|
55
|
+
def threaded_download message
|
56
|
+
return unless queue.any?
|
57
|
+
Logger.info message
|
58
|
+
Logger.notify message do
|
59
|
+
current, count = 0, 10
|
60
|
+
queue.each_slice(count) do |dls|
|
61
|
+
current = current + count
|
62
|
+
Logger.info "#{message} #{Sync.counter[current - count, [current, queue.count].min, queue.count]}\r"
|
63
|
+
dls.map { |block| Thread.new(&block) }.map(&:join)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end end end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Shoperb module Theme module Editor
|
2
|
+
module Sync
|
3
|
+
Pagination = Struct.new("Pagination", :response) do
|
4
|
+
|
5
|
+
def total
|
6
|
+
pagination["total"]
|
7
|
+
end
|
8
|
+
|
9
|
+
def limit
|
10
|
+
pagination["limit"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def offset
|
14
|
+
pagination["offset"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to
|
18
|
+
[total, next_page * limit].min
|
19
|
+
end
|
20
|
+
|
21
|
+
def page
|
22
|
+
offset / limit + 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_page
|
26
|
+
page + 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def present?
|
30
|
+
response && header && total && offset && limit && next_page <= last_page
|
31
|
+
end
|
32
|
+
|
33
|
+
def last_page
|
34
|
+
(total.to_f / limit).ceil
|
35
|
+
end
|
36
|
+
|
37
|
+
def message
|
38
|
+
yield(offset + limit, to, total)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def pagination
|
44
|
+
JSON.parse(header)
|
45
|
+
end
|
46
|
+
|
47
|
+
def header
|
48
|
+
response.headers["x-pagination"]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end end end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module Shoperb module Theme module Editor
|
4
|
+
module Sync
|
5
|
+
extend self
|
6
|
+
|
7
|
+
Editor.autoload_all self, "sync"
|
8
|
+
|
9
|
+
def vendors
|
10
|
+
process Mounter::Model::Vendor do |vendor|
|
11
|
+
vendor.delete("image")
|
12
|
+
vendor
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def collections
|
17
|
+
process Mounter::Model::Collection
|
18
|
+
end
|
19
|
+
|
20
|
+
def countries
|
21
|
+
process Mounter::Model::Country
|
22
|
+
end
|
23
|
+
|
24
|
+
def states
|
25
|
+
process Mounter::Model::State do |state|
|
26
|
+
assign_relation state, Mounter::Model::Country
|
27
|
+
state
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def pages
|
32
|
+
process Mounter::Model::Page
|
33
|
+
end
|
34
|
+
|
35
|
+
def blog_posts
|
36
|
+
process Mounter::Model::BlogCategory, 'blog-categories'
|
37
|
+
process Mounter::Model::BlogPost
|
38
|
+
end
|
39
|
+
|
40
|
+
def media_files
|
41
|
+
process Mounter::Model::MediaFile
|
42
|
+
end
|
43
|
+
|
44
|
+
def media_files
|
45
|
+
process Mounter::Model::MediaFile
|
46
|
+
end
|
47
|
+
|
48
|
+
def addresses(customer_ids)
|
49
|
+
process Mounter::Model::Address, where: "type:AddressWarehouse|AddressShop|AddressVendor|AddressSupplier"
|
50
|
+
lookup = "type:AddressCustomer"
|
51
|
+
if customer_ids.present?
|
52
|
+
lookup = "owner_type:Customer,owner_id:#{customer_ids}"
|
53
|
+
end
|
54
|
+
process Mounter::Model::Address, where: lookup
|
55
|
+
end
|
56
|
+
|
57
|
+
def images
|
58
|
+
Images.process
|
59
|
+
end
|
60
|
+
|
61
|
+
def menus
|
62
|
+
process Mounter::Model::Menu
|
63
|
+
end
|
64
|
+
|
65
|
+
def links
|
66
|
+
process Mounter::Model::Link do |link|
|
67
|
+
assign_relation link, Mounter::Model::Menu
|
68
|
+
link
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def variants(ids)
|
73
|
+
process Mounter::Model::Variant, **product_ids_lookup(ids) do |variant|
|
74
|
+
assign_relation variant, Mounter::Model::Product
|
75
|
+
variant
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def variant_attributes
|
80
|
+
variant_ids = Mounter::Model::Variant.select_map(:id)
|
81
|
+
|
82
|
+
klass = Mounter::Model::VariantAttribute
|
83
|
+
path = klass.to_s.demodulize.tableize
|
84
|
+
opts = {}
|
85
|
+
result = []
|
86
|
+
|
87
|
+
variant_ids.each_slice(50) do |list|
|
88
|
+
opts[:variant_id] = list
|
89
|
+
result += fetch("api/v1/#{path}", **opts).compact
|
90
|
+
end
|
91
|
+
uniq = result.uniq { |h| h[klass.primary_key.to_s] }
|
92
|
+
Logger.info "Received #{result.count} #{path.pluralize(result.count)}, kept #{uniq.count}.\n" if Editor["verbose"]
|
93
|
+
klass.assign uniq.map(&:to_hash)
|
94
|
+
end
|
95
|
+
|
96
|
+
def products(ids)
|
97
|
+
lookup = {}
|
98
|
+
lookup = {where: "id:#{ids}"} if ids.present?
|
99
|
+
process Mounter::Model::ProductType
|
100
|
+
process Mounter::Model::Category
|
101
|
+
process Mounter::Model::Product, **lookup do |product|
|
102
|
+
assign_relation product, Mounter::Model::Category
|
103
|
+
assign_relation product, Mounter::Model::ProductType
|
104
|
+
assign_relation product, Mounter::Model::Vendor
|
105
|
+
product.delete("images")
|
106
|
+
product
|
107
|
+
end
|
108
|
+
product_attributes(ids)
|
109
|
+
variants(ids)
|
110
|
+
variant_attributes
|
111
|
+
end
|
112
|
+
|
113
|
+
def product_attributes(ids)
|
114
|
+
process Mounter::Model::AttributeKey
|
115
|
+
process Mounter::Model::ProductAttribute, **product_ids_lookup(ids) do |product_attribute|
|
116
|
+
assign_relation product_attribute, Mounter::Model::Product
|
117
|
+
product_attribute
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def product_ids_lookup(ids)
|
122
|
+
ids ? { where: "product_id:#{ids}" } : {}
|
123
|
+
end
|
124
|
+
|
125
|
+
def shop
|
126
|
+
process Mounter::Model::Currency
|
127
|
+
process Mounter::Model::Language
|
128
|
+
process Mounter::Model::Shop, "shop" do |shop|
|
129
|
+
assign_relation shop, Mounter::Model::Currency
|
130
|
+
assign_relation shop, Mounter::Model::Language
|
131
|
+
shop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def settings_data
|
136
|
+
data = get_response "api/v1/themes/#{Editor.handle}/settings_data", page: 1 rescue return
|
137
|
+
|
138
|
+
if data.body.presence
|
139
|
+
data = JSON.parse(data.body)
|
140
|
+
File.write('config/settings_data.json', JSON.pretty_generate(data))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def customers(ids)
|
145
|
+
lookup = {}
|
146
|
+
lookup = {where: "id:#{ids}"} if ids.present?
|
147
|
+
process Mounter::Model::Customer, **lookup
|
148
|
+
end
|
149
|
+
|
150
|
+
def customer_groups
|
151
|
+
process Mounter::Model::CustomerGroup
|
152
|
+
#process Mounter::Model::CustomerCustomerGroup
|
153
|
+
end
|
154
|
+
|
155
|
+
def reviews
|
156
|
+
process Mounter::Model::Review do |review|
|
157
|
+
assign_relation review, Mounter::Model::Product
|
158
|
+
assign_relation review, Mounter::Model::Customer
|
159
|
+
review
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def discounts
|
164
|
+
process Mounter::Model::Discount
|
165
|
+
end
|
166
|
+
|
167
|
+
def custom_fields
|
168
|
+
process Mounter::Model::CustomField
|
169
|
+
end
|
170
|
+
|
171
|
+
def subcriptions
|
172
|
+
process Mounter::Model::CustomerSubscriptionPlan
|
173
|
+
process Mounter::Model::CustomerSubscription
|
174
|
+
end
|
175
|
+
|
176
|
+
def process klass, path=klass.to_s.demodulize.tableize, **opts, &block
|
177
|
+
result = fetch("api/v1/#{path}", **opts).map(&(block || ->(this){this})).compact
|
178
|
+
uniq = result.uniq { |h| h[klass.primary_key.to_s] }
|
179
|
+
Logger.info "Received #{result.count} #{path.pluralize(result.count)}, kept #{uniq.count}.\n" if Editor["verbose"]
|
180
|
+
klass.assign uniq.map(&:to_hash)
|
181
|
+
end
|
182
|
+
|
183
|
+
def counter
|
184
|
+
->(from, to, total) { "(#{from} to #{to} of #{total})" }
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def fetch path, message="Syncing #{path}", **opts
|
190
|
+
records, response, page = [], nil, nil
|
191
|
+
Logger.notify message do
|
192
|
+
begin
|
193
|
+
Logger.info "#{message} #{page.message(&counter)}\r" if page
|
194
|
+
response = get_response(path, **opts.merge(page: page))
|
195
|
+
records += Array.wrap(response.parsed)
|
196
|
+
end while (page = Pagination.new(response).presence)
|
197
|
+
end
|
198
|
+
records
|
199
|
+
end
|
200
|
+
|
201
|
+
def get_response path, **opts
|
202
|
+
page = opts[:page]
|
203
|
+
page = page.try(:next_page)
|
204
|
+
opts[:page] = page
|
205
|
+
Api.request path, method: :get, &as_json(**opts)
|
206
|
+
end
|
207
|
+
|
208
|
+
def as_json(params={})
|
209
|
+
->(req) {
|
210
|
+
req.headers["Accept"] = "application/json"
|
211
|
+
req.headers['Current-Shop'] = Editor["oauth-site"]
|
212
|
+
req.options.timeout = 120
|
213
|
+
req.params = params
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
def assign_relation attributes, klass
|
218
|
+
name = klass.to_s.demodulize.underscore
|
219
|
+
id = attributes["#{name}_id"]
|
220
|
+
primary_key = klass.primary_key
|
221
|
+
# attributes["#{name}_#{primary_key}"] = klass.where(id: id).first.try(primary_key) if id
|
222
|
+
|
223
|
+
# apparently where(id: id) still queries not by ID, but by primary key, so:
|
224
|
+
attributes["#{name}_#{primary_key}"] = klass.all.detect { |record| record[:id] == id }.try(primary_key)
|
225
|
+
attributes["#{name}_#{primary_key}"] ||= (attributes["#{name}_id"] = id if id)
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end end end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Shoperb module Theme module Editor
|
2
|
+
module Translations
|
3
|
+
extend self
|
4
|
+
mattr_accessor :locale
|
5
|
+
|
6
|
+
def translate(string, locale: self.locale, **args)
|
7
|
+
key = string.parameterize(separator: ".")
|
8
|
+
(translations[locale.to_s] || HashWithIndifferentAccess.new).fetch(key) { key }
|
9
|
+
end
|
10
|
+
|
11
|
+
alias :t :translate
|
12
|
+
|
13
|
+
def translations
|
14
|
+
Dir[File.join("translations", "*.json")].map do |file|
|
15
|
+
JSON.parse(File.read(file).force_encoding('UTF-8').presence || "{}", object_class: HashWithIndifferentAccess)
|
16
|
+
end.inject({}) do |hash, trans|
|
17
|
+
hash.merge!(trans)
|
18
|
+
hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end end end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Shoperb module Theme module Editor
|
2
|
+
module Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
mattr_accessor :path
|
6
|
+
self.path = Pathname.new("")
|
7
|
+
|
8
|
+
def rel_path path
|
9
|
+
path = Pathname.new(path) unless path.is_a?(Pathname)
|
10
|
+
path.relative_path_from(calc_base(path))
|
11
|
+
end
|
12
|
+
|
13
|
+
def calc_base path
|
14
|
+
path.absolute? ? base : Pathname.new("./")
|
15
|
+
end
|
16
|
+
|
17
|
+
def base
|
18
|
+
Pathname.new(Pathname.new(""))
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_file target
|
22
|
+
File.open(target, "w+b") { |f| f.write(yield) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def mk_tempfile content, *names
|
26
|
+
Tempfile.new(names).tap do |file|
|
27
|
+
file.write(content)
|
28
|
+
file.flush
|
29
|
+
file.open
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def rm_tempfile(file)
|
34
|
+
return unless file
|
35
|
+
if defined?(Tempfile) && file.is_a?(Tempfile)
|
36
|
+
begin
|
37
|
+
file.close unless file.closed?
|
38
|
+
rescue IOError
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
file.unlink
|
42
|
+
rescue Errno::ENOENT, IOError
|
43
|
+
end
|
44
|
+
else
|
45
|
+
path = file.respond_to?(:to_path) ? file.to_path : file.to_s
|
46
|
+
File.delete(path) if path && !path.empty? && File.exist?(path)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end end end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "haml"
|
3
|
+
require "rack"
|
4
|
+
require "pry"
|
5
|
+
require "shoperb_liquid"
|
6
|
+
|
7
|
+
require_relative 'shoperb_theme_editor/ext'
|
8
|
+
|
9
|
+
autoload :OpenStruct, "ostruct"
|
10
|
+
autoload :FileUtils, "fileutils"
|
11
|
+
autoload :Zip, "zip"
|
12
|
+
autoload :Pathname, "pathname"
|
13
|
+
autoload :Tempfile, "tempfile"
|
14
|
+
autoload :Time, "time"
|
15
|
+
autoload :OAuth2, "oauth2"
|
16
|
+
autoload :Faraday, "faraday"
|
17
|
+
autoload :Launchy, "launchy"
|
18
|
+
autoload :I18n, "i18n"
|
19
|
+
autoload :CoffeeScript, "coffee_script"
|
20
|
+
autoload :Sass, "sass"
|
21
|
+
autoload :ActionView, "action_view"
|
22
|
+
autoload :WEBrick, "webrick"
|
23
|
+
autoload :ActionDispatch, "action_dispatch"
|
24
|
+
autoload :Sinatra, "sinatra"
|
25
|
+
autoload :YAML, "yaml"
|
26
|
+
autoload :JSON, "json"
|
27
|
+
autoload :Logger, "logger"
|
28
|
+
autoload :URI, "uri"
|
29
|
+
autoload :Mime, "action_dispatch/http/mime_type"
|
30
|
+
autoload :Slop, "slop"
|
31
|
+
autoload :Sequel, "sequel"
|
32
|
+
require 'faraday/multipart'
|
33
|
+
require_relative 'shoperb_theme_editor/ext/sequel.rb'
|
34
|
+
|
35
|
+
module Shoperb module Theme
|
36
|
+
module Editor
|
37
|
+
extend self
|
38
|
+
mattr_accessor :config
|
39
|
+
delegate :[], :[]=, :reset, to: :config
|
40
|
+
|
41
|
+
def with_configuration(options, *args)
|
42
|
+
self.config = Configuration.new(options.to_hash.select { |_, value| !value.nil? }, *args)
|
43
|
+
`mkdir -p #{Utils.base + "data"}`
|
44
|
+
Sequel::Model.db = Sequel.sqlite((Utils.base + "data/data.db").to_s, setup_regexp_function: true)
|
45
|
+
begin
|
46
|
+
yield
|
47
|
+
rescue Interrupt => e
|
48
|
+
exit(130)
|
49
|
+
rescue Exception => e
|
50
|
+
Error.report(e)
|
51
|
+
ensure
|
52
|
+
self.config.save
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def autoload_all mod, folder
|
57
|
+
Gem.find_files(
|
58
|
+
Pathname.new("shoperb_theme_editor/#{folder}/*.rb").cleanpath.to_s
|
59
|
+
).each { |path|
|
60
|
+
name = Pathname.new(path).basename(".rb")
|
61
|
+
mod.autoload :"#{name.to_s.sub(/.*\./, '').camelize}",
|
62
|
+
Pathname.new("shoperb_theme_editor/#{folder}/#{name}").cleanpath.to_s
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
autoload_all self, "/"
|
67
|
+
|
68
|
+
def handle content=local_spec_content
|
69
|
+
spec(content)["handle"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def spec content=local_spec_content
|
73
|
+
JSON.parse(content)
|
74
|
+
end
|
75
|
+
|
76
|
+
def settings_data
|
77
|
+
unless File.exist?('config/settings_data.json')
|
78
|
+
if File.exist?('presets/default.json')
|
79
|
+
File.open('config/settings_data.json', 'w') do |f|
|
80
|
+
settings = JSON.parse(File.read('presets/default.json'))['settings']
|
81
|
+
f.write JSON.pretty_generate({ general: settings })
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if File.exist?('config/settings_data.json')
|
87
|
+
JSON.parse(File.read('config/settings_data.json'))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def local_spec_content
|
92
|
+
@local_spec_content ||= begin
|
93
|
+
if File.exist?(path = Utils.base + "config/spec.json")
|
94
|
+
File.read(path)
|
95
|
+
else
|
96
|
+
new_spec_content
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# if spec is missing (for old themes)
|
102
|
+
def new_spec_content
|
103
|
+
content = JSON.parse(File.read(path = Utils.base + '.shoperb'))
|
104
|
+
spec_content = {
|
105
|
+
handle: content['handle'],
|
106
|
+
compile: {
|
107
|
+
stylesheets: ['application.css'],
|
108
|
+
javascripts: ['application.js']
|
109
|
+
}
|
110
|
+
}.to_json
|
111
|
+
Dir.mkdir 'config' unless File.exist?('config')
|
112
|
+
File.open('config/spec.json', 'w') {|f| f.write(spec_content) }
|
113
|
+
spec_content
|
114
|
+
end
|
115
|
+
|
116
|
+
# general theme settings (styles)
|
117
|
+
def theme_settings
|
118
|
+
(settings_data ? settings_data['general'].presence : nil) || {}
|
119
|
+
end
|
120
|
+
|
121
|
+
def presets
|
122
|
+
res = []
|
123
|
+
|
124
|
+
Pathname.glob(Utils.base + "presets/*.json") do |path|
|
125
|
+
content = File.read(path)
|
126
|
+
data = JSON.parse(content) rescue { "settings" => { } }
|
127
|
+
|
128
|
+
res.push([nil, data["settings"]]) if data["default"]
|
129
|
+
res.push([data["name_key"], data["settings"]])
|
130
|
+
end
|
131
|
+
|
132
|
+
res.to_h
|
133
|
+
end
|
134
|
+
|
135
|
+
def compiler asset_url, digests: true, **options
|
136
|
+
Artisans::ThemeCompiler.new(
|
137
|
+
File.expand_path(Utils.base),
|
138
|
+
asset_url,
|
139
|
+
settings: theme_settings,
|
140
|
+
compile: spec["compile"],
|
141
|
+
file_reader: SprocketsFileReader.new(digests: digests)
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
class SprocketsFileReader
|
146
|
+
def initialize(digests: true)
|
147
|
+
@digests = digests
|
148
|
+
end
|
149
|
+
|
150
|
+
def read(file)
|
151
|
+
File.read(file) if File.file?(file)
|
152
|
+
end
|
153
|
+
|
154
|
+
def find_digest(path)
|
155
|
+
@digests && File.exist?(path) ? Digest::MD5.hexdigest(File.read(path)) : ''
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end end
|