discourse_theme 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c42bbee99e84d60c23d79bdb0dd289d9b2be382bdfd1c609c77b31f7c689ccda
4
- data.tar.gz: 5c1c7bf95a1c83fe52d677ed766cb77c5d764d6e559f493e71b9bf2e7672bd1c
3
+ metadata.gz: a5c78e3947eb3207c1deec8de4326ca852efabb4c45c9dc616ff8cee76695747
4
+ data.tar.gz: d5024d27d0f687d6300f4c34cfd3ac6c72ef5049f03f10e15397e9d45dcd8bbf
5
5
  SHA512:
6
- metadata.gz: e65319a323e96446890fd013c642bfadcc5fce7e623097a37cb731d5f20f4f9ea7b107adfa8a3db555935a0ee332a244b161c87f9e93cc54618695881e147d42
7
- data.tar.gz: 9706b11b32d297faddd26875a78f6fb9e9b0816e152197f4698ca50c4c03c033406579045cfde2dd5fea34e102ee10f89300e6f8d619bfe168f601c8f1ce8eeb
6
+ metadata.gz: 5aff9397fbe13616f9012bef9e3f41dc4ea0587d09ae8f040e806d7ef51c9f79dc71e911ec0808e16a631a46636ff72ebec244ef543b7a69852bd24265cb40fd
7
+ data.tar.gz: ff3cc429ef37e484c96af103d130140b54c42a07e74a222270fd1f533ede61a3b9e8ee1bf8a4fbeb94d1e047e95481251561ad821eaa58001d2c02fe38d89189
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  Gemfile.lock
10
+
11
+ .rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml
data/.rubocop.yml CHANGED
@@ -1,110 +1 @@
1
- AllCops:
2
- TargetRubyVersion: 2.2
3
- DisabledByDefault: true
4
- Exclude:
5
- - 'bundle/**/*'
6
- - 'pkg/**/*'
7
-
8
- # Prefer &&/|| over and/or.
9
- Style/AndOr:
10
- Enabled: true
11
-
12
- # Do not use braces for hash literals when they are the last argument of a
13
- # method call.
14
- Style/BracesAroundHashParameters:
15
- Enabled: true
16
-
17
- # Align `when` with `case`.
18
- Layout/CaseIndentation:
19
- Enabled: true
20
-
21
- # Align comments with method definitions.
22
- Layout/CommentIndentation:
23
- Enabled: true
24
-
25
- # No extra empty lines.
26
- Layout/EmptyLines:
27
- Enabled: true
28
-
29
- # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
30
- Style/HashSyntax:
31
- Enabled: true
32
-
33
- # Two spaces, no tabs (for indentation).
34
- Layout/IndentationWidth:
35
- Enabled: true
36
-
37
- Layout/SpaceAfterColon:
38
- Enabled: true
39
-
40
- Layout/SpaceAfterComma:
41
- Enabled: true
42
-
43
- Layout/SpaceAroundEqualsInParameterDefault:
44
- Enabled: true
45
-
46
- Layout/SpaceAroundKeyword:
47
- Enabled: true
48
-
49
- Layout/SpaceAroundOperators:
50
- Enabled: true
51
-
52
- Layout/SpaceBeforeFirstArg:
53
- Enabled: true
54
-
55
- # Defining a method with parameters needs parentheses.
56
- Style/MethodDefParentheses:
57
- Enabled: true
58
-
59
- # Use `foo {}` not `foo{}`.
60
- Layout/SpaceBeforeBlockBraces:
61
- Enabled: true
62
-
63
- # Use `foo { bar }` not `foo {bar}`.
64
- Layout/SpaceInsideBlockBraces:
65
- Enabled: true
66
-
67
- # Use `{ a: 1 }` not `{a:1}`.
68
- Layout/SpaceInsideHashLiteralBraces:
69
- Enabled: true
70
-
71
- Layout/SpaceInsideParens:
72
- Enabled: true
73
-
74
- # Detect hard tabs, no hard tabs.
75
- Layout/Tab:
76
- Enabled: true
77
-
78
- # Blank lines should not have any spaces.
79
- Layout/TrailingBlankLines:
80
- Enabled: true
81
-
82
- # No trailing whitespace.
83
- Layout/TrailingWhitespace:
84
- Enabled: true
85
-
86
- Lint/Debugger:
87
- Enabled: true
88
-
89
- Layout/BlockAlignment:
90
- Enabled: true
91
-
92
- # Align `end` with the matching keyword or starting expression except for
93
- # assignments, where it should be aligned with the LHS.
94
- Layout/EndAlignment:
95
- Enabled: true
96
- EnforcedStyleAlignWith: variable
97
-
98
- # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
99
- Lint/RequireParentheses:
100
- Enabled: true
101
-
102
- Layout/MultilineMethodCallIndentation:
103
- Enabled: true
104
- EnforcedStyle: indented
105
-
106
- Layout/AlignHash:
107
- Enabled: true
108
-
109
- Bundler/OrderedGems:
110
- Enabled: false
1
+ inherit_from: https://raw.githubusercontent.com/discourse/discourse/master/.rubocop.yml
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/**/*_test.rb"]
8
8
  end
9
9
 
10
- task :default => :test
10
+ task default: :test
data/bin/discourse_theme CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  require_relative '../lib/discourse_theme'
4
4
 
5
- DiscourseTheme::Cli.new.run
5
+ DiscourseTheme::Cli.new.run(ARGV)
@@ -23,15 +23,17 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
25
25
 
26
- spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "bundler", "~> 2.0"
27
27
  spec.add_development_dependency "rake", "~> 10.0"
28
28
  spec.add_development_dependency "minitest", "~> 5.0"
29
29
  spec.add_development_dependency "guard", "~> 2.14"
30
30
  spec.add_development_dependency "guard-minitest", "~> 2.4"
31
+ spec.add_development_dependency "webmock", "~> 3.5"
31
32
 
32
33
  spec.add_dependency "minitar", "~> 0.6"
33
34
  spec.add_dependency "listen", "~> 3.1"
34
35
  spec.add_dependency "multipart-post", "~> 2.0"
36
+ spec.add_dependency "tty-prompt", "~> 0.18"
35
37
 
36
38
  spec.required_ruby_version = '>= 2.2.0'
37
39
  end
@@ -11,13 +11,17 @@ require 'uri'
11
11
  require 'listen'
12
12
  require 'json'
13
13
  require 'yaml'
14
+ require 'tty/prompt'
14
15
 
15
16
  require 'discourse_theme/version'
16
17
  require 'discourse_theme/config'
17
18
  require 'discourse_theme/cli'
19
+ require 'discourse_theme/client'
20
+ require 'discourse_theme/downloader'
18
21
  require 'discourse_theme/uploader'
19
22
  require 'discourse_theme/watcher'
20
23
  require 'discourse_theme/scaffold'
21
24
 
22
25
  module DiscourseTheme
26
+ class ThemeError < StandardError; end
23
27
  end
@@ -1,123 +1,148 @@
1
- class DiscourseTheme::Cli
1
+ module DiscourseTheme
2
+ class Cli
2
3
 
3
- SETTINGS_FILE = File.expand_path("~/.discourse_theme")
4
+ @@prompt = ::TTY::Prompt.new(help_color: :cyan)
5
+ @@pastel = Pastel.new
4
6
 
5
- def usage
6
- puts "Usage: discourse_theme COMMAND"
7
- puts
8
- puts "discourse_theme new DIR : Creates a new theme in the designated directory"
9
- puts "discourse_theme watch DIR : Watches the theme directory and synchronizes with Discourse"
10
- exit 1
11
- end
7
+ def self.yes?(message)
8
+ @@prompt.yes?(@@pastel.cyan("? ") + message)
9
+ end
12
10
 
13
- def guess_api_key(settings)
14
- api_key = ENV['DISCOURSE_API_KEY']
15
- if api_key
16
- puts "Using api_key provided by DISCOURSE_API_KEY"
11
+ def self.ask(message, default: nil)
12
+ @@prompt.ask(@@pastel.cyan("? ") + message, default: default)
17
13
  end
18
14
 
19
- if !api_key && settings.api_key
20
- api_key = settings.api_key
21
- puts "Using previously stored api key in #{SETTINGS_FILE}"
15
+ def self.select(message, options)
16
+ @@prompt.select(@@pastel.cyan("? ") + message, options)
22
17
  end
23
18
 
24
- if !api_key
25
- puts "No API key found in DISCOURSE_API_KEY env var enter your API key: "
26
- api_key = STDIN.gets.strip
27
- puts "Would you like me to store this API key in #{SETTINGS_FILE}? (Yes|No)"
28
- answer = STDIN.gets.strip
29
- if answer =~ /y(es)?/i
30
- settings.api_key = api_key
31
- end
19
+ def self.info(message)
20
+ puts @@pastel.blue("i ") + message
32
21
  end
33
22
 
34
- api_key
35
- end
23
+ def self.progress(message)
24
+ puts @@pastel.yellow("» ") + message
25
+ end
36
26
 
37
- def is_https_redirect?(url)
38
- url = URI.parse(url)
39
- path = url.path
40
- path = "/" if path.empty?
41
- req = Net::HTTP::Get.new("/")
42
- response = Net::HTTP.start(url.host, url.port) { |http| http.request(req) }
43
- Net::HTTPRedirection === response && response['location'] =~ /^https/i
44
- end
27
+ def self.error(message)
28
+ puts @@pastel.red("✘ #{message}")
29
+ end
45
30
 
46
- def guess_url(settings)
47
- url = ENV['DISCOURSE_URL']
48
- if url
49
- puts "Site provided by DISCOURSE_URL"
31
+ def self.success(message)
32
+ puts @@pastel.green("✔ #{message}")
50
33
  end
51
34
 
52
- if !url && settings.url
53
- url = settings.url
54
- puts "Using #{url} defined in #{SETTINGS_FILE}"
35
+ SETTINGS_FILE = File.expand_path("~/.discourse_theme")
36
+
37
+ def usage
38
+ puts "Usage: discourse_theme COMMAND [--reset]"
39
+ puts
40
+ puts "discourse_theme new DIR : Creates a new theme in the designated directory"
41
+ puts "discourse_theme download DIR : Download a theme from the server, and store in the designated directory"
42
+ puts "discourse_theme watch DIR : Watches the theme directory and synchronizes with Discourse"
43
+ puts
44
+ puts "Use --reset to change the configuration for a directory"
45
+ exit 1
55
46
  end
56
47
 
57
- if !url
58
- puts "No site found! Where would you like to synchronize the theme to: "
59
- url = STDIN.gets.strip
60
- url = "http://#{url}" unless url =~ /^https?:\/\//
48
+ def run(args)
49
+ usage unless args[1]
61
50
 
62
- # maybe this is an HTTPS redirect
63
- uri = URI.parse(url)
64
- if URI::HTTP === uri && uri.port == 80 && is_https_redirect?(url)
65
- puts "Detected an #{url} is an HTTPS domain"
66
- url = url.sub("http", "https")
67
- end
51
+ reset = !!args.delete("--reset")
68
52
 
69
- puts "Would you like me to store this site name at: #{SETTINGS_FILE}? (Yes|No)"
70
- answer = STDIN.gets.strip
71
- if answer =~ /y(es)?/i
72
- settings.url = url
73
- end
74
- end
53
+ command = args[0].to_s.downcase
54
+ dir = File.expand_path(args[1])
75
55
 
76
- url
77
- end
56
+ config = DiscourseTheme::Config.new(SETTINGS_FILE)
57
+ settings = config[dir]
78
58
 
79
- def run
80
- usage unless ARGV[1]
59
+ theme_id = settings.theme_id
81
60
 
82
- command = ARGV[0].to_s.downcase
83
- dir = File.expand_path(ARGV[1])
61
+ if command == "new"
62
+ raise DiscourseTheme::ThemeError.new "'#{dir} is not empty" if Dir.exists?(dir) && !Dir.empty?(dir)
63
+ DiscourseTheme::Scaffold.generate(dir)
64
+ if Cli.yes?("Would you like to start 'watching' this theme?")
65
+ args[0] = "watch"
66
+ Cli.progress "Running discourse_theme #{args.join(' ')}"
67
+ run(args)
68
+ end
69
+ elsif command == "watch"
70
+ raise DiscourseTheme::ThemeError.new "'#{dir} does not exist" unless Dir.exists?(dir)
71
+ client = DiscourseTheme::Client.new(dir, settings, reset: reset)
84
72
 
85
- dir_exists = File.exist?(dir)
73
+ theme_list = client.get_themes_list
86
74
 
87
- if command == "new" && !dir_exists
88
- DiscourseTheme::Scaffold.generate(dir)
89
- elsif command == "watch" && dir_exists
90
- if !File.exist?("#{dir}/about.json")
91
- puts "No about.json file found in #{dir}!"
92
- puts
93
- usage
94
- end
75
+ options = {}
76
+ if theme_id && theme = theme_list.find { |t| t["id"] == theme_id }
77
+ options["Sync with existing theme: '#{theme["name"]}' (id:#{theme_id})"] = :default
78
+ end
79
+ options["Create and sync with a new theme"] = :create
80
+ options["Select a different theme"] = :select
95
81
 
96
- config = DiscourseTheme::Config.new(SETTINGS_FILE)
97
- settings = config[dir]
82
+ choice = Cli.select('How would you like to sync this theme?', options.keys)
98
83
 
99
- url = guess_url(settings)
100
- api_key = guess_api_key(settings)
84
+ if options[choice] == :create
85
+ theme_id = nil
86
+ elsif options[choice] == :select
87
+ themes = render_theme_list(theme_list)
88
+ choice = Cli.select('Which theme would you like to sync with?', themes)
89
+ theme_id = extract_theme_id(choice)
90
+ end
101
91
 
102
- if !url
103
- puts "Missing site to synchronize with!"
104
- usage
105
- end
92
+ uploader = DiscourseTheme::Uploader.new(dir: dir, client: client, theme_id: theme_id)
93
+
94
+ Cli.progress "Uploading theme from #{dir}"
95
+ settings.theme_id = theme_id = uploader.upload_full_theme
96
+
97
+ Cli.success "Theme uploaded (id:#{theme_id})"
98
+ watcher = DiscourseTheme::Watcher.new(dir: dir, uploader: uploader)
99
+
100
+ Cli.progress "Watching for changes in #{dir}..."
101
+ watcher.watch
102
+
103
+ elsif command == "download"
104
+ client = DiscourseTheme::Client.new(dir, settings, reset: reset)
105
+ downloader = DiscourseTheme::Downloader.new(dir: dir, client: client)
106
+
107
+ FileUtils.mkdir_p dir unless Dir.exists?(dir)
108
+ raise DiscourseTheme::ThemeError.new "'#{dir} is not empty" unless Dir.empty?(dir)
106
109
 
107
- if !api_key
108
- puts "Missing api key!"
110
+ Cli.progress "Loading theme list..."
111
+ themes = render_theme_list(client.get_themes_list)
112
+
113
+ choice = Cli.select('Which theme would you like to download?', themes)
114
+ theme_id = extract_theme_id(choice)
115
+
116
+ Cli.progress "Downloading theme into #{dir}"
117
+
118
+ downloader.download_theme(theme_id)
119
+ settings.theme_id = theme_id
120
+
121
+ Cli.success "Theme downloaded"
122
+
123
+ if Cli.yes?("Would you like to start 'watching' this theme?")
124
+ args[0] = "watch"
125
+ Cli.progress "Running discourse_theme #{args.join(' ')}"
126
+ run(args)
127
+ end
128
+ else
109
129
  usage
110
130
  end
111
131
 
112
- uploader = DiscourseTheme::Uploader.new(dir: dir, api_key: api_key, site: url)
113
- print "Uploading theme from #{dir} to #{url} : "
114
- uploader.upload_full_theme
132
+ Cli.progress "Exiting..."
133
+ rescue DiscourseTheme::ThemeError => e
134
+ Cli.error "#{e.message}"
135
+ rescue Interrupt, TTY::Reader::InputInterrupt => e
136
+ Cli.error "Interrupted"
137
+ end
115
138
 
116
- watcher = DiscourseTheme::Watcher.new(dir: dir, uploader: uploader)
139
+ def render_theme_list(themes)
140
+ themes.sort_by { |t| t["updated_at"] }
141
+ .reverse.map { |theme| "#{theme["name"]} (id:#{theme["id"]})" }
142
+ end
117
143
 
118
- watcher.watch
119
- else
120
- usage
144
+ def extract_theme_id(rendered_name)
145
+ /\(id:([0-9]+)\)$/.match(rendered_name)[1].to_i
121
146
  end
122
147
  end
123
148
  end
@@ -0,0 +1,183 @@
1
+ module DiscourseTheme
2
+ class Client
3
+ THEME_CREATOR_REGEX = /^https:\/\/theme-creator.discourse.org$/i
4
+
5
+ def initialize(dir, settings, reset:)
6
+ @reset = reset
7
+ @url = guess_url(settings)
8
+ @api_key = guess_api_key(settings)
9
+
10
+ raise "Missing site to synchronize with!" if !@url
11
+ raise "Missing api key!" if !@api_key
12
+
13
+ @is_theme_creator = !!(THEME_CREATOR_REGEX =~ @url)
14
+
15
+ parts = discourse_version.split(".").map { |s| s.sub('beta', '').to_i }
16
+ if parts[0] < 2 || parts[1] < 2 || parts[2] < 0 || (!parts[3].nil? && parts[3] < 10)
17
+ Cli.info "discourse_theme is designed for Discourse 2.2.0.beta10 or above"
18
+ Cli.info "download will not function, and syncing destination will be unpredictable"
19
+ end
20
+ end
21
+
22
+ def get_themes_list
23
+ endpoint = root +
24
+ if @is_theme_creator
25
+ "/user_themes.json"
26
+ else
27
+ "/admin/customize/themes.json?api_key=#{@api_key}"
28
+ end
29
+
30
+ response = request(Net::HTTP::Get.new(endpoint), never_404: true)
31
+ JSON.parse(response.body)["themes"]
32
+ end
33
+
34
+ def get_raw_theme_export(id)
35
+ endpoint = root +
36
+ if @is_theme_creator
37
+ "/user_themes/#{id}/export"
38
+ else
39
+ "/admin/customize/themes/#{id}/export?api_key=#{@api_key}"
40
+ end
41
+
42
+ response = request(Net::HTTP::Get.new endpoint)
43
+ raise "Error downloading theme: #{response.code}" unless response.code.to_i == 200
44
+ response.body
45
+ end
46
+
47
+ def update_theme(id, args)
48
+ endpoint = root +
49
+ if @is_theme_creator
50
+ "/user_themes/#{id}"
51
+ else
52
+ "/admin/themes/#{id}?api_key=#{@api_key}"
53
+ end
54
+
55
+ put = Net::HTTP::Put.new(endpoint, 'Content-Type' => 'application/json')
56
+ put.body = args.to_json
57
+ request(put)
58
+ end
59
+
60
+ def upload_full_theme(tgz, theme_id:)
61
+ endpoint = root +
62
+ if @is_theme_creator
63
+ "/user_themes/import.json"
64
+ else
65
+ "/admin/themes/import.json?api_key=#{@api_key}"
66
+ end
67
+
68
+ post = Net::HTTP::Post::Multipart.new(
69
+ endpoint,
70
+ "theme_id" => theme_id,
71
+ "bundle" => UploadIO.new(tgz, "application/tar+gzip", "bundle.tar.gz")
72
+ )
73
+ request(post)
74
+ end
75
+
76
+ def discourse_version
77
+ endpoint = root +
78
+ if @is_theme_creator
79
+ "/about.json"
80
+ else
81
+ "/about.json?api_key=#{@api_key}"
82
+ end
83
+
84
+ response = request(Net::HTTP::Get.new(endpoint), never_404: true)
85
+ json = JSON.parse(response.body)
86
+ json["about"]["version"]
87
+ end
88
+
89
+ private
90
+
91
+ def root
92
+ @url
93
+ end
94
+
95
+ def request(request, never_404: false)
96
+ uri = URI.parse(@url)
97
+ http = Net::HTTP.new(uri.host, uri.port)
98
+ http.use_ssl = URI::HTTPS === uri
99
+ add_headers(request)
100
+ http.request(request).tap do |response|
101
+ if response.code == '404' && never_404
102
+ raise DiscourseTheme::ThemeError.new "Error: Incorrect site URL, or API key does not have the correct privileges"
103
+ elsif !['200', '201'].include?(response.code)
104
+ errors = JSON.parse(response.body)["errors"].join(', ') rescue nil
105
+ raise DiscourseTheme::ThemeError.new "Error #{response.code} for #{request.path.split("?")[0]}#{(": " + errors) if errors}"
106
+ end
107
+ end
108
+ rescue Errno::ECONNREFUSED
109
+ raise DiscourseTheme::ThemeError.new "Connection refused for #{request.path}"
110
+ end
111
+
112
+ def add_headers(request)
113
+ if @is_theme_creator
114
+ request["User-Api-Key"] = @api_key
115
+ end
116
+ end
117
+
118
+ def guess_url(settings)
119
+ url = ENV['DISCOURSE_URL']
120
+ if url
121
+ Cli.progress "Using #{url} from DISCOURSE_URL"
122
+ end
123
+
124
+ if !url && settings.url
125
+ url = settings.url
126
+ Cli.progress "Using #{url} from #{DiscourseTheme::Cli::SETTINGS_FILE}"
127
+ end
128
+
129
+ if !url || @reset
130
+ url = Cli.ask("What is the root URL of your Discourse site?", default: url).strip
131
+ url = "http://#{url}" unless url =~ /^https?:\/\//
132
+
133
+ # maybe this is an HTTPS redirect
134
+ uri = URI.parse(url)
135
+ if URI::HTTP === uri && uri.port == 80 && is_https_redirect?(url)
136
+ Cli.info "Detected that #{url} should be accessed over https"
137
+ url = url.sub("http", "https")
138
+ end
139
+
140
+ if Cli.yes?("Would you like this site name stored in #{DiscourseTheme::Cli::SETTINGS_FILE}?")
141
+ settings.url = url
142
+ else
143
+ settings.url = nil
144
+ end
145
+ end
146
+
147
+ url
148
+ end
149
+
150
+ def guess_api_key(settings)
151
+ api_key = ENV['DISCOURSE_API_KEY']
152
+ if api_key
153
+ Cli.progress "Using api key from DISCOURSE_API_KEY"
154
+ end
155
+
156
+ if !api_key && settings.api_key
157
+ api_key = settings.api_key
158
+ Cli.progress "Using api key from #{DiscourseTheme::Cli::SETTINGS_FILE}"
159
+ end
160
+
161
+ if !api_key || @reset
162
+ api_key = Cli.ask("What is your API key?", default: api_key).strip
163
+ if Cli.yes?("Would you like this API key stored in #{DiscourseTheme::Cli::SETTINGS_FILE}?")
164
+ settings.api_key = api_key
165
+ else
166
+ settings.api_key = nil
167
+ end
168
+ end
169
+
170
+ api_key
171
+ end
172
+
173
+ def is_https_redirect?(url)
174
+ url = URI.parse(url)
175
+ path = url.path
176
+ path = "/" if path.empty?
177
+ req = Net::HTTP::Get.new("/")
178
+ response = Net::HTTP.start(url.host, url.port) { |http| http.request(req) }
179
+ Net::HTTPRedirection === response && response['location'] =~ /^https/i
180
+ end
181
+
182
+ end
183
+ end
@@ -7,11 +7,11 @@ class DiscourseTheme::Config
7
7
  end
8
8
 
9
9
  def api_key
10
- safe_config["api_key"]
10
+ search_api_key(url) || safe_config["api_key"]
11
11
  end
12
12
 
13
13
  def api_key=(val)
14
- set("api_key", val)
14
+ set_api_key(url, val)
15
15
  end
16
16
 
17
17
  def url
@@ -22,6 +22,14 @@ class DiscourseTheme::Config
22
22
  set("url", val)
23
23
  end
24
24
 
25
+ def theme_id
26
+ safe_config["theme_id"].to_i
27
+ end
28
+
29
+ def theme_id=(theme_id)
30
+ set("theme_id", theme_id.to_i)
31
+ end
32
+
25
33
  protected
26
34
 
27
35
  def set(name, val)
@@ -39,6 +47,18 @@ class DiscourseTheme::Config
39
47
  {}
40
48
  end
41
49
  end
50
+
51
+ def search_api_key(url)
52
+ hash = @config.raw_config["api_keys"]
53
+ hash[url] if hash
54
+ end
55
+
56
+ def set_api_key(url, api_key)
57
+ hash = @config.raw_config["api_keys"] ||= {}
58
+ hash[url] = api_key
59
+ @config.save
60
+ api_key
61
+ end
42
62
  end
43
63
 
44
64
  attr_reader :raw_config, :filename
@@ -59,13 +79,6 @@ class DiscourseTheme::Config
59
79
  end
60
80
  end
61
81
 
62
- def set(path, url:, api_key:)
63
- @raw_config[path] = {
64
- "url" => url,
65
- "api_key" => api_key
66
- }
67
- end
68
-
69
82
  def save
70
83
  File.write(@filename, @raw_config.to_yaml)
71
84
  end
@@ -0,0 +1,31 @@
1
+ class DiscourseTheme::Downloader
2
+
3
+ def initialize(dir:, client:)
4
+ @dir = dir
5
+ @client = client
6
+ @theme_id = nil
7
+ end
8
+
9
+ def download_theme(id)
10
+ raw = @client.get_raw_theme_export(id)
11
+ sio = StringIO.new(raw)
12
+ gz = Zlib::GzipReader.new(sio)
13
+ Minitar.unpack(gz, @dir)
14
+
15
+ # Minitar extracts into a sub directory, move all the files up one dir
16
+ Dir.chdir(@dir) do
17
+ folders = Dir.glob('*/')
18
+ raise "Extraction failed" unless folders.length == 1
19
+ FileUtils.mv(Dir.glob("#{folders[0]}*"), "./")
20
+ FileUtils.remove_dir(folders[0])
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def add_headers(request)
27
+ if @is_theme_creator
28
+ request["User-Api-Key"] = @api_key
29
+ end
30
+ end
31
+ end
@@ -1,36 +1,39 @@
1
- class DiscourseTheme::Scaffold
1
+ module DiscourseTheme
2
+ class Scaffold
2
3
 
3
- BLANK_FILES = %w{
4
- common/common.scss
5
- common/header.html
6
- common/after_header.html
7
- common/footer.html
8
- common/head_tag.html
9
- common/body_tag.html
10
- common/embedded.scss
4
+ BLANK_FILES = %w{
5
+ common/common.scss
6
+ common/header.html
7
+ common/after_header.html
8
+ common/footer.html
9
+ common/head_tag.html
10
+ common/body_tag.html
11
+ common/embedded.scss
11
12
 
12
- desktop/desktop.scss
13
- desktop/header.html
14
- desktop/after_header.html
15
- desktop/footer.html
16
- desktop/head_tag.html
17
- desktop/body_tag.html
13
+ desktop/desktop.scss
14
+ desktop/header.html
15
+ desktop/after_header.html
16
+ desktop/footer.html
17
+ desktop/head_tag.html
18
+ desktop/body_tag.html
18
19
 
19
- mobile/mobile.scss
20
- mobile/header.html
21
- mobile/after_header.html
22
- mobile/footer.html
23
- mobile/head_tag.html
24
- mobile/body_tag.html
20
+ mobile/mobile.scss
21
+ mobile/header.html
22
+ mobile/after_header.html
23
+ mobile/footer.html
24
+ mobile/head_tag.html
25
+ mobile/body_tag.html
25
26
 
26
- settings.yml
27
- }
27
+ locales/en.yml
28
28
 
29
- ABOUT_JSON = <<~STR
29
+ settings.yml
30
+ }
31
+
32
+ ABOUT_JSON = <<~STR
30
33
  {
31
34
  "name": "#NAME#",
32
- "about_url": "URL",
33
- "license_url": "URL",
35
+ "about_url": null,
36
+ "license_url": null,
34
37
  "assets": {
35
38
  },
36
39
  "color_schemes": {
@@ -38,39 +41,35 @@ class DiscourseTheme::Scaffold
38
41
  }
39
42
  STR
40
43
 
41
- HELP = <<~STR
42
- Are you a bit lost? Be sure to read https://meta.discourse.org/t/how-to-develop-custom-themes/60848
43
- STR
44
+ HELP = <<~STR
45
+ Are you a bit lost? Be sure to read https://meta.discourse.org/t/how-to-develop-custom-themes/60848
46
+ STR
44
47
 
45
- GIT_IGNORE = <<~STR
48
+ GIT_IGNORE = <<~STR
46
49
  .discourse-site
47
50
  HELP
48
51
  STR
49
52
 
50
- def self.generate(dir)
51
- puts "Generating a scaffold theme at #{dir}"
53
+ def self.generate(dir)
54
+ Cli.progress "Generating a scaffold theme at #{dir}"
52
55
 
53
- puts "What would you like to call your theme? "
54
- name = STDIN.gets.strip
55
- if name.length == 0
56
- puts "Please pick a name"
57
- exit 1
58
- end
56
+ name = Cli.ask("What would you like to call your theme?").strip
59
57
 
60
- FileUtils.mkdir_p dir
61
- Dir.chdir dir do
62
- File.write('about.json', ABOUT_JSON.sub("#NAME#", name))
63
- File.write('HELP', HELP)
64
- File.write('.gitignore', GIT_IGNORE)
58
+ FileUtils.mkdir_p dir
59
+ Dir.chdir dir do
60
+ File.write('about.json', ABOUT_JSON.sub("#NAME#", name))
61
+ File.write('HELP', HELP)
62
+ File.write('.gitignore', GIT_IGNORE)
65
63
 
66
- BLANK_FILES.each do |f|
67
- puts "#{f}"
68
- FileUtils.mkdir_p File.dirname(f)
69
- FileUtils.touch f
70
- end
64
+ BLANK_FILES.each do |f|
65
+ Cli.info "Creating #{f}"
66
+ FileUtils.mkdir_p File.dirname(f)
67
+ FileUtils.touch f
68
+ end
71
69
 
72
- puts "Initializing git repo"
73
- puts `git init .`
70
+ Cli.info "Initializing git repo"
71
+ puts `git init .`
72
+ end
74
73
  end
75
74
  end
76
75
  end
@@ -2,12 +2,10 @@ class DiscourseTheme::Uploader
2
2
 
3
3
  THEME_CREATOR_REGEX = /^https:\/\/theme-creator.discourse.org$/i
4
4
 
5
- def initialize(dir:, api_key:, site:)
5
+ def initialize(dir:, client:, theme_id: nil)
6
6
  @dir = dir
7
- @api_key = api_key
8
- @site = site
9
- @is_theme_creator = !!(THEME_CREATOR_REGEX =~ site)
10
- @theme_id = nil
7
+ @client = client
8
+ @theme_id = theme_id
11
9
  end
12
10
 
13
11
  def compress_dir(gzip, dir)
@@ -36,16 +34,15 @@ class DiscourseTheme::Uploader
36
34
  puts
37
35
  end
38
36
  count += 1
39
- puts
40
- puts "Error in #{row["target"]} #{row["name"]}: #{row["error"]}"
41
- puts
37
+ Cli.error
38
+ Cli.error "Error in #{row["target"]} #{row["name"]}: #{row["error"]}"
39
+ Cli.error
42
40
  end
43
41
  end
44
42
  count
45
43
  end
46
44
 
47
45
  def upload_theme_field(target: , name: , type_id: , value:)
48
-
49
46
  raise "expecting theme_id to be set!" unless @theme_id
50
47
 
51
48
  args = {
@@ -59,77 +56,29 @@ class DiscourseTheme::Uploader
59
56
  }
60
57
  }
61
58
 
62
- endpoint =
63
- if @is_theme_creator
64
- "/user_themes/#{@theme_id}"
65
- else
66
- "/admin/themes/#{@theme_id}?api_key=#{@api_key}"
59
+ response = @client.update_theme(@theme_id, args)
60
+ json = JSON.parse(response.body)
61
+ if diagnose_errors(json) != 0
62
+ Cli.error "(end of errors)"
67
63
  end
68
-
69
- uri = URI.parse(@site + endpoint)
70
- http = Net::HTTP.new(uri.host, uri.port)
71
- http.use_ssl = URI::HTTPS === uri
72
-
73
- request = Net::HTTP::Put.new(uri.request_uri, 'Content-Type' => 'application/json')
74
- request.body = args.to_json
75
- add_headers(request)
76
- http.start do |h|
77
- response = h.request(request)
78
- if response.code.to_i == 200
79
- json = JSON.parse(response.body)
80
- if diagnose_errors(json) == 0
81
- puts "(done)"
82
- end
83
- else
84
- puts "Error importing field status: #{response.code}"
85
- end
86
- end
87
64
  end
88
65
 
89
66
  def upload_full_theme
90
67
  filename = "#{Pathname.new(Dir.tmpdir).realpath}/bundle_#{SecureRandom.hex}.tar.gz"
91
68
  compress_dir(filename, @dir)
92
69
 
93
- endpoint =
94
- if @is_theme_creator
95
- "/user_themes/import.json"
96
- else
97
- "/admin/themes/import.json?api_key=#{@api_key}"
98
- end
99
-
100
- uri = URI.parse(@site + endpoint)
101
- http = Net::HTTP.new(uri.host, uri.port)
102
- http.use_ssl = URI::HTTPS === uri
103
70
  File.open(filename) do |tgz|
71
+ response = @client.upload_full_theme(tgz, theme_id: @theme_id)
104
72
 
105
- request = Net::HTTP::Post::Multipart.new(
106
- uri.request_uri,
107
- "bundle" => UploadIO.new(tgz, "application/tar+gzip", "bundle.tar.gz"),
108
- )
109
- add_headers(request)
110
- response = http.request(request)
111
- if response.code.to_i == 201
112
- json = JSON.parse(response.body)
113
- @theme_id = json["theme"]["id"]
114
- if diagnose_errors(json) == 0
115
- puts "(done)"
116
- end
117
- else
118
- puts "Error importing theme status: #{response.code}"
119
-
120
- puts response.body
73
+ json = JSON.parse(response.body)
74
+ @theme_id = json["theme"]["id"]
75
+ if diagnose_errors(json) != 0
76
+ Cli.error "(end of errors)"
121
77
  end
78
+ @theme_id
122
79
  end
123
-
124
80
  ensure
125
81
  FileUtils.rm_f filename
126
82
  end
127
83
 
128
- private
129
-
130
- def add_headers(request)
131
- if @is_theme_creator
132
- request["User-Api-Key"] = @api_key
133
- end
134
- end
135
84
  end
@@ -1,3 +1,3 @@
1
1
  module DiscourseTheme
2
- VERSION = "0.1.8"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,51 +1,63 @@
1
- class DiscourseTheme::Watcher
2
- def initialize(dir:, uploader:)
3
- @dir = dir
4
- @uploader = uploader
5
- end
6
-
7
- def watch
8
- listener = Listen.to(@dir) do |modified, added, removed|
9
- if modified.length == 1 &&
10
- added.length == 0 &&
11
- removed.length == 0 &&
12
- (resolved = resolve_file(modified[0]))
13
-
14
- target, name, type_id = resolved
15
- print "Updating #{target} #{name}: "
16
-
17
- @uploader.upload_theme_field(
18
- target: target,
19
- name: name,
20
- value: File.read(modified[0]),
21
- type_id: type_id
22
- )
23
- else
24
- print "Full re-sync is required, re-uploading theme: "
25
- @uploader.upload_full_theme
26
- end
1
+ module DiscourseTheme
2
+ class Watcher
3
+ def initialize(dir:, uploader:)
4
+ @dir = dir
5
+ @uploader = uploader
27
6
  end
28
7
 
29
- listener.start
30
- sleep
8
+ def watch
9
+ listener = Listen.to(@dir) do |modified, added, removed|
10
+ begin
11
+ if modified.length == 1 &&
12
+ added.length == 0 &&
13
+ removed.length == 0 &&
14
+ (resolved = resolve_file(modified[0]))
15
+
16
+ target, name, type_id = resolved
17
+ Cli.progress "Fast updating #{target}.scss"
18
+
19
+ @uploader.upload_theme_field(
20
+ target: target,
21
+ name: name,
22
+ value: File.read(modified[0]),
23
+ type_id: type_id
24
+ )
25
+ else
26
+ count = modified.length + added.length + removed.length
27
+ if count > 1
28
+ Cli.progress "Detected changes in #{count} files, uploading theme"
29
+ else
30
+ Cli.progress "Detected changes in #{modified[0].gsub(@dir, '')}, uploading theme"
31
+ end
32
+ @uploader.upload_full_theme
33
+ end
34
+ Cli.success "Done! Watching for changes..."
35
+ rescue DiscourseTheme::ThemeError => e
36
+ Cli.error "#{e.message}"
37
+ Cli.progress "Watching for changes..."
38
+ end
39
+ end
31
40
 
32
- end
41
+ listener.start
42
+ sleep
43
+ end
33
44
 
34
- protected
45
+ protected
35
46
 
36
- def resolve_file(path)
37
- dir_len = File.expand_path(@dir).length
38
- name = File.expand_path(path)[dir_len + 1..-1]
47
+ def resolve_file(path)
48
+ dir_len = File.expand_path(@dir).length
49
+ name = File.expand_path(path)[dir_len + 1..-1]
39
50
 
40
- target, file = name.split("/")
51
+ target, file = name.split("/")
41
52
 
42
- if ["common", "desktop", "mobile"].include?(target)
43
- if file == "#{target}.scss"
44
- # a CSS file
45
- return [target, "scss", 1]
53
+ if ["common", "desktop", "mobile"].include?(target)
54
+ if file == "#{target}.scss"
55
+ # a CSS file
56
+ return [target, "scss", 1]
57
+ end
46
58
  end
47
- end
48
59
 
49
- nil
60
+ nil
61
+ end
50
62
  end
51
63
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discourse_theme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-22 00:00:00.000000000 Z
11
+ date: 2019-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.16'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.16'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.5'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: minitar
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
138
  version: '2.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: tty-prompt
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.18'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.18'
125
153
  description: CLI helper for creating Discourse themes
126
154
  email:
127
155
  - sam.saffron@gmail.com
@@ -143,7 +171,9 @@ files:
143
171
  - discourse_theme.gemspec
144
172
  - lib/discourse_theme.rb
145
173
  - lib/discourse_theme/cli.rb
174
+ - lib/discourse_theme/client.rb
146
175
  - lib/discourse_theme/config.rb
176
+ - lib/discourse_theme/downloader.rb
147
177
  - lib/discourse_theme/scaffold.rb
148
178
  - lib/discourse_theme/uploader.rb
149
179
  - lib/discourse_theme/version.rb
@@ -168,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
198
  version: '0'
169
199
  requirements: []
170
200
  rubyforge_project:
171
- rubygems_version: 2.7.6
201
+ rubygems_version: 2.7.8
172
202
  signing_key:
173
203
  specification_version: 4
174
204
  summary: CLI helper for creating Discourse themes