diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a99563b7100d72999112be8a8da7d17c84649bb0..635298bc24a30ee593efe479b849e26975f0e3a6 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, + :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 1e557c476384426d04cc35ef5f92b92bf50bb7f6..cce2a847b53890385e4be6a99cabffe447c6fc64 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController :color_scheme_id, :layout, :dashboard, - :project_view + :project_view, + :theme_id ) end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index d36bb4ab07495fc706d539388d3ddfe661bfba84..64605908c055d6dd14f9be71f5eeb385b5c1793a 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -40,6 +40,10 @@ module PreferencesHelper ] end + def user_application_theme + Gitlab::Themes.for_user(current_user).css_class + end + def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/models/user.rb b/app/models/user.rb index 105eb62f1fa9b7a57aa36b447312dc7be98ca1f5..f75cc21c65c8d0df9a72b7409d2bc230beb794a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,7 @@ class User < ActiveRecord::Base default_value_for :project_view, :files default_value_for :notified_of_own_activity, false default_value_for :preferred_language, I18n.default_locale + default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e96610908448e5c3be88c82994410ef690db746f..1ca0f263f133b9e44949465ad87e16528e2a63ff 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,6 +76,14 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace + ## Default theme ID + ## 1 - Graphite + ## 2 - Charcoal + ## 3 - Green + ## 4 - Gray + ## 5 - Violet + ## 6 - Blue + # default_theme: 2 # default: 2 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. @@ -741,4 +749,4 @@ test: admin_group: '' staging: - <<: *base + <<: *base \ No newline at end of file diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7c1ca05a57b7e068b78468ca0eb5b6cdd495d58c..40fbdd3ef9b748f2f37cebea2028351a7848a5b5 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? diff --git a/doc/api/keys.md b/doc/api/keys.md index 376ac27df3a2ea4e07c3b45edb8ad08d8ebb32d3..ddcf7830621444aa2a9e68906c79ff6dc3024715 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -32,6 +32,7 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/session.md b/doc/api/session.md index f79eac11689f4fe68de2162ad74e048db87fc91e..b97e26f34a2c71b88aaf3e9f664327a76bf221ac 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -39,6 +39,7 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index 9f3e4caf2f476dd01d36c843eeefa3fc6ca11544..6d5db16b36ad15e28abdc8dc7603460a2c6a1b72 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,6 +72,7 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -105,6 +106,7 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 3, "projects_limit": 100, @@ -215,6 +217,7 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -341,6 +344,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -387,6 +391,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1d224d7bc217c7fdcca350c05438f01cadeb73cc..d5f2c422c584c9daf50fda5953ebcaee894a04fa 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -35,7 +35,7 @@ module API expose :confirmed_at expose :last_activity_on expose :email - expose :color_scheme_id, :projects_limit, :current_sign_in_at + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb new file mode 100644 index 0000000000000000000000000000000000000000..19ab76ae80f6a6b26d3e5c694e76511b14700499 --- /dev/null +++ b/lib/gitlab/themes.rb @@ -0,0 +1,87 @@ +module Gitlab + # Module containing GitLab's application theme definitions and helper methods + # for accessing them. + module Themes + extend self + + # Theme ID used when no `default_theme` configuration setting is provided. + APPLICATION_DEFAULT = 2 + + # Struct class representing a single Theme + Theme = Struct.new(:id, :name, :css_class) + + # All available Themes + THEMES = [ + Theme.new(1, 'Graphite', 'ui_graphite'), + Theme.new(2, 'Charcoal', 'ui_charcoal'), + Theme.new(3, 'Green', 'ui_green'), + Theme.new(4, 'Black', 'ui_black'), + Theme.new(5, 'Violet', 'ui_violet'), + Theme.new(6, 'Blue', 'ui_blue') + ].freeze + + # Convenience method to get a space-separated String of all the theme + # classes that might be applied to the `body` element + # + # Returns a String + def body_classes + THEMES.collect(&:css_class).uniq.join(' ') + end + + # Get a Theme by its ID + # + # If the ID is invalid, returns the default Theme. + # + # id - Integer ID + # + # Returns a Theme + def by_id(id) + THEMES.detect { |t| t.id == id } || default + end + + # Returns the number of defined Themes + def count + THEMES.size + end + + # Get the default Theme + # + # Returns a Theme + def default + by_id(default_id) + end + + # Iterate through each Theme + # + # Yields the Theme object + def each(&block) + THEMES.each(&block) + end + + # Get the Theme for the specified user, or the default + # + # user - User record + # + # Returns a Theme + def for_user(user) + if user + by_id(user.theme_id) + else + default + end + end + + private + + def default_id + id = Gitlab.config.gitlab.default_theme.to_i + + # Prevent an invalid configuration setting from causing an infinite loop + if id < THEMES.first.id || id > THEMES.last.id + APPLICATION_DEFAULT + else + id + end + end + end +end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index a5f544b4f9225eab6ae6193eee4e095550f606bb..a66b4ab09022454ddceb7b309b7fd0c15b6885e4 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,7 +25,8 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '1' ) patch :update, user: params, format: format @@ -40,7 +41,8 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '2' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(prefs) diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index 6181b3ccc8694545c49994cacfc80002688c4fbe..e6c1d9c9d845ab527f03a768f4c98a60ace92d20 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -19,6 +19,7 @@ "organization", "last_sign_in_at", "confirmed_at", + "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index a04c87b08eb77c1f8c06411c2c5d4c622bb2d125..50ac3886a36271779e7df86e2f80fd533afdfd95 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -26,6 +26,32 @@ describe PreferencesHelper do end end + describe 'user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + stub_user(theme_id: 3) + + expect(helper.user_application_theme).to eq 'ui_green' + end + + it 'returns the default when id is invalid' do + stub_user(theme_id: Gitlab::Themes.count + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) + + expect(helper.user_application_theme).to eq 'ui_charcoal' + end + end + + context 'without a user' do + it 'returns the default theme' do + stub_user + + expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7a140518dd2448f348fa46907157d7caab6da44c --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_graphite') + expect(css).to include(' ui_charcoal ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Graphite' + expect(described_class.by_id(6).name).to eq 'Blue' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index abf732e60bf8889fa9f03d564f177d0b35707979..961f891f559d8630e70a9f25e36a1bfe5bcbb64c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -716,6 +716,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end @@ -726,6 +727,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index cd55d63125e5975316bf1504b1a9192d1c278039..688175369ae5bdea2f471b80740e87716d24244b 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index cd55d63125e5975316bf1504b1a9192d1c278039..ce8dfe5ae7559b755a4735f36c93b146cc8430bc 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} +} \ No newline at end of file