FEATURE: Rake themes installer (#7848)

FEATURE: Rake themes installer (#7848)

  • Delete remote_theme when deleting the theme

  • Install themes and theme components through rake

  • Removed unnecessary test

diff --git a/app/models/theme.rb b/app/models/theme.rb
index 81a3478..798bb2e 100644
--- a/app/models/theme.rb
+++ b/app/models/theme.rb
@@ -22,7 +22,7 @@ class Theme < ActiveRecord::Base
   has_many :child_themes, -> { order(:name) }, through: :child_theme_relation, source: :child_theme
   has_many :parent_themes, -> { order(:name) }, through: :parent_theme_relation, source: :parent_theme
   has_many :color_schemes
-  belongs_to :remote_theme, autosave: true
+  belongs_to :remote_theme, autosave: true, dependent: :destroy
 
   has_one :settings_field, -> { where(target_id: Theme.targets[:settings], name: "yaml") }, class_name: 'ThemeField'
   has_one :javascript_cache, dependent: :destroy
diff --git a/app/services/themes_install_task.rb b/app/services/themes_install_task.rb
new file mode 100644
index 0000000..1a3ac1b
--- /dev/null
+++ b/app/services/themes_install_task.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class ThemesInstallTask
+  def self.install(yml)
+    counts = { installed: 0, updated: 0, skipped: 0, errors: 0 }
+    log = []
+    themes = YAML::load(yml)
+    themes.each do |theme|
+      name = theme[0]
+      val = theme[1]
+      installer = new(val)
+
+      if installer.theme_exists?
+        log << "#{name}: is already installed"
+        counts[:skipped] += 1
+      else
+        begin
+          installer.install
+          log << "#{name}: installed from #{installer.url}"
+          counts[:installed] += 1
+        rescue RemoteTheme::ImportError, Discourse::InvalidParameters => err
+          log << "#{name}: #{err.message}"
+          counts[:errors] += 1
+        end
+      end
+    end
+
+    return log, counts
+  end
+
+  attr_reader :url, :options
+
+  def initialize(url_or_options = nil)
+    if url_or_options.is_a?(Hash)
+      @url = url_or_options.fetch("url")
+      @options = url_or_options
+    else
+      @url = url_or_options
+      @options = {}
+    end
+  end
+
+  def theme_exists?
+    RemoteTheme
+      .where(remote_url: url)
+      .where(branch: options.fetch("branch", nil))
+      .exists?
+  end
+
+  def install
+    theme = RemoteTheme.import_theme(url, Discourse.system_user, private_key: options["private_key"], branch: options["branch"])
+    theme.set_default! if options.fetch("default", false)
+  end
+end
diff --git a/lib/tasks/themes.rake b/lib/tasks/themes.rake
new file mode 100644
index 0000000..bce2136
--- /dev/null
+++ b/lib/tasks/themes.rake
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'yaml'
+
+# == YAML file format
+#
+# 2 different formats are accepted:
+#
+# theme_name: https://github.com/example/theme.git
+#
+# theme_name:
+#   url: https://github.com/example/theme.git
+#   branch: abc
+#   private_key: ...
+#   default: true
+#
+# In the second form, only the url is required.
+#
+desc "Install themes & theme components"
+task "themes:install" => :environment do
+  yml = (STDIN.tty?) ? '' : STDIN.read
+  if yml == ''
+    puts
+    puts "Please specify a themes yml file"
+    puts "Example: rake themes:install < themes.yml"
+    exit 1
+  end
+
+  log, counts = ThemesInstallTask.install(yml)
+
+  puts log
+
+  puts
+  puts "Results:"
+  puts " Installed: #{counts[:installed]}"
+  puts " Updated:   #{counts[:updated]}"
+  puts " Skipped:   #{counts[:skipped]}"
+  puts " Errors:    #{counts[:errors]}"
+
+  if counts[:errors] > 0
+    exit 1
+  end
+end
diff --git a/spec/services/themes_spec.rb b/spec/services/themes_spec.rb
new file mode 100644
index 0000000..cabf942
--- /dev/null
+++ b/spec/services/themes_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ThemesInstallTask do
+
+  before do
+    Discourse::Application.load_tasks
+  end
+
+  let(:github_repo) { 'https://github.com/example/theme.git' }
+  let(:branch) { 'master' }
+
+  describe '.new' do
+    context 'with url' do
+      subject { described_class.new(github_repo) }
+
+      it 'configures the url' do
+        expect(subject.url).to eq github_repo
+      end
+
+      it 'initializes without options' do
+        expect(subject.options).to eq({})
+      end
+    end
+
+    context 'with options' do
+      subject { described_class.new(options) }
+      let(:options) { { 'url' => github_repo, 'branch' => branch } }
+
+      it 'configures the url' do
+        expect(subject.url).to eq github_repo
+      end
+
+      it 'initializes options' do
+        expect(subject.options).to eq("url" => github_repo, "branch" => branch)
+      end
+    end
+  end
+
+  describe '#theme_exists?' do
+    let(:theme) { Fabricate(:theme) }
+    subject { described_class.new(options) }
+
+    context 'without branch' do
+      let(:options) { github_repo }
+
+      it 'returns true when a branchless theme exists' do
+        theme.create_remote_theme(remote_url: github_repo)
+        expect(subject.theme_exists?).to be true
+      end
+
+      it 'returns false when the url exists but with a branch' do
+        theme.create_remote_theme(remote_url: github_repo, branch: branch)
+        expect(subject.theme_exists?).to be false
+      end
+
+      it 'returns false when it doesnt exist' do
+        theme.create_remote_theme(remote_url: 'https://github.com/example/different_theme.git')
+        expect(subject.theme_exists?).to be false
+      end
+    end
+
+    context 'with branch' do
+      let(:options) { { 'url' => github_repo, 'branch' => branch } }
+
+      it 'returns false when a branchless theme exists' do
+        theme.create_remote_theme(remote_url: github_repo)
+        expect(subject.theme_exists?).to be false
+      end
+
+      it 'returns true when the url exists with a branch' do
+        theme.create_remote_theme(remote_url: github_repo, branch: branch)
+        expect(subject.theme_exists?).to be true
+      end
+
+      it 'returns false when it doesnt exist' do
+        theme.create_remote_theme(remote_url: 'https://github.com/example/different_theme.git')
+        expect(subject.theme_exists?).to be false
+      end
+    end
+  end
+end

GitHub sha: 1318e0b2

1 Like