diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index da56e3cdbc..b7ebe6a5c8 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -55,7 +55,7 @@ class Dispatcher when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() MergeRequests.init() - when 'dashboard:show' + when 'dashboard:show', 'root:show' new Dashboard() new Activities() when 'dashboard:projects:starred' diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 40459a9a15..bb0b66b86e 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,10 +1,8 @@ class @Profile constructor: -> - $('.edit_user .application-theme input, .edit_user .code-preview-theme input').click -> - # Submit the form - $('.edit_user').submit() - - new Flash("Appearance settings saved", "notice") + # Automatically submit the Preferences form when any of its radio buttons change + $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> + $(this).parents('form').submit() $('.update-username form').on 'ajax:before', -> $('.loading-gif').show() @@ -18,7 +16,6 @@ class @Profile $('.update-notifications').on 'ajax:complete', -> $(this).find('.btn-save').enable() - $('.js-choose-user-avatar-button').bind "click", -> form = $(this).closest("form") form.find(".js-user-avatar-input").click() diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb new file mode 100644 index 0000000000..538b09ca54 --- /dev/null +++ b/app/controllers/profiles/preferences_controller.rb @@ -0,0 +1,38 @@ +class Profiles::PreferencesController < Profiles::ApplicationController + before_action :user + + def show + end + + def update + begin + if @user.update_attributes(preferences_params) + flash[:notice] = 'Preferences saved.' + else + flash[:alert] = 'Failed to save preferences.' + end + rescue ArgumentError => e + # Raised when `dashboard` is given an invalid value. + flash[:alert] = "Failed to save preferences (#{e.message})." + end + + respond_to do |format| + format.html { redirect_to profile_preferences_path } + format.js + end + end + + private + + def user + @user = current_user + end + + def preferences_params + params.require(:user).permit( + :color_scheme_id, + :dashboard, + :theme_id + ) + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f4366c18e7..b4af9e490e 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -8,9 +8,6 @@ class ProfilesController < Profiles::ApplicationController def show end - def design - end - def applications @applications = current_user.oauth_applications @authorized_tokens = current_user.oauth_authorized_tokens @@ -29,7 +26,6 @@ class ProfilesController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to :back } - format.js end end @@ -65,10 +61,21 @@ class ProfilesController < Profiles::ApplicationController def user_params params.require(:user).permit( - :email, :password, :password_confirmation, :bio, :name, - :username, :skype, :linkedin, :twitter, :website_url, - :color_scheme_id, :theme_id, :avatar, :hide_no_ssh_key, - :hide_no_password, :location, :public_email + :avatar, + :bio, + :email, + :hide_no_password, + :hide_no_ssh_key, + :linkedin, + :location, + :name, + :password, + :password_confirmation, + :public_email, + :skype, + :twitter, + :username, + :website_url ) end end diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb new file mode 100644 index 0000000000..fdfe00dc13 --- /dev/null +++ b/app/controllers/root_controller.rb @@ -0,0 +1,28 @@ +# RootController +# +# This controller exists solely to handle requests to `root_url`. When a user is +# logged in and has customized their `dashboard` setting, they will be +# redirected to their preferred location. +# +# For users who haven't customized the setting, we simply delegate to +# `DashboardController#show`, which is the default. +class RootController < DashboardController + before_action :redirect_to_custom_dashboard, only: [:show] + + def show + super + end + + private + + def redirect_to_custom_dashboard + return unless current_user + + case current_user.dashboard + when 'stars' + redirect_to starred_dashboard_projects_path + else + return + end + end +end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb new file mode 100644 index 0000000000..f58e2c6635 --- /dev/null +++ b/app/helpers/preferences_helper.rb @@ -0,0 +1,24 @@ +# Helper methods for per-User preferences +module PreferencesHelper + # Maps `dashboard` values to more user-friendly option text + DASHBOARD_CHOICES = { + projects: 'Your Projects (default)', + stars: 'Starred Projects' + }.with_indifferent_access.freeze + + # Returns an Array usable by a select field for more user-friendly option text + def dashboard_choices + defined = User.dashboards + + if defined.size != DASHBOARD_CHOICES.size + # Ensure that anyone adding new options updates this method too + raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + + " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." + else + defined.map do |key, _| + # Use `fetch` so `KeyError` gets raised when a key is missing + [DASHBOARD_CHOICES.fetch(key), key] + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 8be0b62270..6ac287203b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,12 +50,13 @@ # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) # location :string(255) +# public_email :string(255) default(""), not null # encrypted_otp_secret :string(255) # encrypted_otp_secret_iv :string(255) # encrypted_otp_secret_salt :string(255) # otp_required_for_login :boolean # otp_backup_codes :text -# public_email :string(255) default(""), not null +# dashboard :integer default(0) # require 'carrierwave/orm/activerecord' @@ -701,4 +702,8 @@ class User < ActiveRecord::Base def can_be_removed? !solo_owned_groups.present? end + + # User's Dashboard preference + # Note: When adding an option, it MUST go on the end of the array. + enum dashboard: [:projects, :stars] end diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index d46dba4a24..687c1fc3dd 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar - = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do - = link_to root_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = nav_link(path: ['dashboard#show', 'root#show'], html_options: {class: 'home'}) do + = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = icon('dashboard fw') %span Your Projects diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index ac37fd4c1c..121665bd53 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -38,11 +38,12 @@ %span SSH Keys %span.count= current_user.keys.count - = nav_link(path: 'profiles#design') do - = link_to design_profile_path, title: 'Design', data: {placement: 'right'} do + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do + -# TODO (rspeicher): Better icon? = icon('image fw') %span - Design + Preferences = nav_link(path: 'profiles#history') do = link_to history_profile_path, title: 'History', data: {placement: 'right'} do = icon('history fw') diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/preferences/show.html.haml similarity index 54% rename from app/views/profiles/design.html.haml rename to app/views/profiles/preferences/show.html.haml index f450ec1c01..18692bd32a 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,11 +1,13 @@ -- page_title "Design" +- page_title 'Preferences' %h3.page-title = page_title %p.light - Appearance settings will be saved to your profile and made available across all devices. + These settings allow you to customize the appearance and behavior of the site. + They are saved with your account and will persist to any device you use to + access the site. %hr -= form_for @user, url: profile_path, remote: true, method: :put do |f| += form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f| .panel.panel-default.application-theme .panel-heading Application theme @@ -50,7 +52,19 @@ .code_highlight_opts - color_schemes.each do |color_scheme_id, color_scheme| = label_tag do - .prev - = image_tag "#{color_scheme}-scheme-preview.png" + .prev= image_tag "#{color_scheme}-scheme-preview.png" = f.radio_button :color_scheme_id, color_scheme_id - = color_scheme.gsub(/[-_]+/, ' ').humanize + = color_scheme.tr('-_', ' ').titleize + + .panel.panel-default + .panel-heading + Behavior + .panel-body + .form-group + = f.label :dashboard, class: 'control-label' do + Default Dashboard + = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') + .col-sm-10 + = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + .panel-footer + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/preferences/update.js.erb similarity index 54% rename from app/views/profiles/update.js.erb rename to app/views/profiles/preferences/update.js.erb index db37619136..5b7b92aac4 100644 --- a/app/views/profiles/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,3 +1,9 @@ // Remove body class for any previous theme, re-add current one $('body').removeClass('<%= Gitlab::Theme.body_classes %>') $('body').addClass('<%= app_theme %> <%= theme_type %>') + +// Re-enable the "Save" button +$('input[type=submit]').enable() + +// Show the notice flash message +new Flash('<%= flash.discard(:notice) %>', 'notice') diff --git a/config/routes.rb b/config/routes.rb index f4a104664f..d60bc796fd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -203,7 +203,6 @@ Gitlab::Application.routes.draw do resource :profile, only: [:show, :update] do member do get :history - get :design get :applications put :reset_private_token @@ -222,6 +221,7 @@ Gitlab::Application.routes.draw do put :reset end end + resource :preferences, only: [:show, :update] resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] @@ -293,7 +293,7 @@ Gitlab::Application.routes.draw do get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error end - root to: "dashboard#show" + root to: "root#show" # # Project Area diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb new file mode 100644 index 0000000000..2628e45072 --- /dev/null +++ b/db/migrate/20150610065936_add_dashboard_to_users.rb @@ -0,0 +1,9 @@ +class AddDashboardToUsers < ActiveRecord::Migration + def up + add_column :users, :dashboard, :integer, default: 0 + end + + def down + remove_column :users, :dashboard + end +end diff --git a/db/schema.rb b/db/schema.rb index aea0742cf3..f063a4868b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150529150354) do +ActiveRecord::Schema.define(version: 20150610065936) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -29,12 +29,13 @@ ActiveRecord::Schema.define(version: 20150529150354) do t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false + t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" t.boolean "user_oauth_applications", default: true t.string "after_sign_out_path" + t.integer "session_expire_delay", default: 10080, null: false end create_table "broadcast_messages", force: true do |t| @@ -495,12 +496,13 @@ ActiveRecord::Schema.define(version: 20150529150354) do t.string "bitbucket_access_token" t.string "bitbucket_access_token_secret" t.string "location" - t.string "public_email", default: "", null: false t.string "encrypted_otp_secret" t.string "encrypted_otp_secret_iv" t.string "encrypted_otp_secret_salt" t.boolean "otp_required_for_login" t.text "otp_backup_codes" + t.string "public_email", default: "", null: false + t.integer "dashboard", default: 0 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/README.md b/doc/README.md index 7a2181edde..2845961325 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,35 +1,36 @@ -# Documentation - -## User documentation - -- [API](api/README.md) Automate GitLab via a simple and powerful API. -- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. -- [Importing to GitLab](workflow/importing/README.md). -- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. -- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. -- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. -- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. -- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. -- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. - -## Administrator documentation - -- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. -- [Install](install/README.md) Requirements, directory structures and installation from source. -- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. -- [Log system](logs/logs.md) Log system. -- [Operations](operations/README.md) Keeping GitLab up and running -- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. -- [Security](security/README.md) Learn what you can do to further secure your GitLab instance. -- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. -- [Update](update/README.md) Update guides to upgrade your installation. -- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. - -## Contributor documentation - -- [Development](development/README.md) Explains the architecture and the guidelines for shell commands. -- [Legal](legal/README.md) Contributor license agreements. -- [Release](release/README.md) How to make the monthly and security releases. \ No newline at end of file +# Documentation + +## User documentation + +- [API](api/README.md) Automate GitLab via a simple and powerful API. +- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. +- [Importing to GitLab](workflow/importing/README.md). +- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. +- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. +- [Profile Settings](profile/README.md) +- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. +- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. +- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. +- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. + +## Administrator documentation + +- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. +- [Install](install/README.md) Requirements, directory structures and installation from source. +- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. +- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Log system](logs/logs.md) Log system. +- [Operations](operations/README.md) Keeping GitLab up and running +- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. +- [Security](security/README.md) Learn what you can do to further secure your GitLab instance. +- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. +- [Update](update/README.md) Update guides to upgrade your installation. +- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. + +## Contributor documentation + +- [Development](development/README.md) Explains the architecture and the guidelines for shell commands. +- [Legal](legal/README.md) Contributor license agreements. +- [Release](release/README.md) How to make the monthly and security releases. diff --git a/doc/workflow/2fa.png b/doc/profile/2fa.png similarity index 100% rename from doc/workflow/2fa.png rename to doc/profile/2fa.png diff --git a/doc/workflow/2fa_auth.png b/doc/profile/2fa_auth.png similarity index 100% rename from doc/workflow/2fa_auth.png rename to doc/profile/2fa_auth.png diff --git a/doc/profile/README.md b/doc/profile/README.md new file mode 100644 index 0000000000..6f8359d87f --- /dev/null +++ b/doc/profile/README.md @@ -0,0 +1,4 @@ +# Profile Settings + +- [Preferences](preferences.md) +- [Two-factor Authentication (2FA)](two_factor_authentication.md) diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md new file mode 100644 index 0000000000..ce5f193678 --- /dev/null +++ b/doc/profile/preferences.md @@ -0,0 +1,32 @@ +# Profile Preferences + +Settings in the **Profile > Preferences** page allow the user to customize +various aspects of the site to their liking. + +## Application theme + +Changing this setting allows the user to customize the color scheme used for the +navigation bar on the left side of the screen. + +The default is **Charcoal**. + +## Syntax highlighting theme + +Changing this setting allows the user to customize the theme used when viewing +syntax highlighted code on the site. + +The default is **White**. + +## Behavior + +### Default Dashboard + +For users who have access to a large number of projects but only keep up with a +select few, the amount of activity on the default Dashboard page can be +overwhelming. + +Changing this setting allows the user to redefine what their default dashboard +will be. Setting it to **Starred Projects** will make that Dashboard view the +default when signing in or clicking the application logo in the upper left. + +The default is **Your Projects**. diff --git a/doc/workflow/two_factor_authentication.md b/doc/profile/two_factor_authentication.md similarity index 100% rename from doc/workflow/two_factor_authentication.md rename to doc/profile/two_factor_authentication.md diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 70a8179c8e..f1959d3013 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,16 +1,15 @@ -# Workflow - -- [Authorization for merge requests](authorization_for_merge_requests.md) -- [Change your time zone](timezone.md) -- [Feature branch workflow](workflow.md) -- [GitLab Flow](gitlab_flow.md) -- [Groups](groups.md) -- [Keyboard shortcuts](shortcuts.md) -- [Labels](labels.md) -- [Notifications](notifications.md) -- [Project Features](project_features.md) -- [Project forking workflow](forking_workflow.md) -- [Protected branches](protected_branches.md) -- [Two-factor Authentication (2FA)](two_factor_authentication.md) -- [Web Editor](web_editor.md) -- ["Work In Progress" Merge Requests](wip_merge_requests.md) \ No newline at end of file +# Workflow + +- [Authorization for merge requests](authorization_for_merge_requests.md) +- [Change your time zone](timezone.md) +- [Feature branch workflow](workflow.md) +- [GitLab Flow](gitlab_flow.md) +- [Groups](groups.md) +- [Keyboard shortcuts](shortcuts.md) +- [Labels](labels.md) +- [Notifications](notifications.md) +- [Project Features](project_features.md) +- [Project forking workflow](forking_workflow.md) +- [Protected branches](protected_branches.md) +- [Web Editor](web_editor.md) +- ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature index 7801ae5b8c..1fa4ac88dd 100644 --- a/features/profile/active_tab.feature +++ b/features/profile/active_tab.feature @@ -18,9 +18,9 @@ Feature: Profile Active Tab Then the active main tab should be SSH Keys And no other main tabs should be active - Scenario: On Profile Design - Given I visit profile design page - Then the active main tab should be Design + Scenario: On Profile Preferences + Given I visit profile preferences page + Then the active main tab should be Preferences And no other main tabs should be active Scenario: On Profile History diff --git a/features/profile/profile.feature b/features/profile/profile.feature index d586167cdf..0dd0afde8b 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -84,16 +84,3 @@ Feature: Profile Then I visit profile applications page And I click to remove application Then I see that application is removed - - @javascript - Scenario: I change my application theme - Given I visit profile design page - When I change my application theme - Then I should see the theme change immediately - And I should receive feedback that the changes were saved - - @javascript - Scenario: I change my code preview theme - Given I visit profile design page - When I change my code preview theme - Then I should receive feedback that the changes were saved diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 8595ee876a..79e3b55f6e 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -15,8 +15,8 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps ensure_active_main_tab('SSH Keys') end - step 'the active main tab should be Design' do - ensure_active_main_tab('Design') + step 'the active main tab should be Preferences' do + ensure_active_main_tab('Preferences') end step 'the active main tab should be History' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 32e6859eff..649aea8e3f 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -114,27 +114,6 @@ class Spinach::Features::Profile < Spinach::FeatureSteps expect(page).to have_content "#{current_user.name} closed issue" end - step "I change my application theme" do - page.within '.application-theme' do - choose "Violet" - end - end - - step "I change my code preview theme" do - page.within '.code-preview-theme' do - choose "Solarized dark" - end - end - - step "I should see the theme change immediately" do - expect(page).to have_selector('body.ui_color') - expect(page).not_to have_selector('body.ui_basic') - end - - step "I should receive feedback that the changes were saved" do - expect(page).to have_content("saved") - end - step 'my password is expired' do current_user.update_attributes(password_expires_at: Time.now - 1.hour) end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 09ae7e3a30..3bd0d60281 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -123,8 +123,8 @@ module SharedPaths visit profile_keys_path end - step 'I visit profile design page' do - visit design_profile_path + step 'I visit profile preferences page' do + visit profile_preferences_path end step 'I visit profile history page' do diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb new file mode 100644 index 0000000000..1f0943c93d --- /dev/null +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Profiles::PreferencesController do + let(:user) { create(:user) } + + before do + sign_in(user) + + allow(subject).to receive(:current_user).and_return(user) + end + + describe 'GET show' do + it 'renders' do + get :show + expect(response).to render_template :show + end + + it 'assigns user' do + get :show + expect(assigns[:user]).to eq user + end + end + + describe 'PATCH update' do + def go(params: {}, format: :js) + params.reverse_merge!( + color_scheme_id: '1', + dashboard: 'stars', + theme_id: '1' + ) + + patch :update, user: params, format: format + end + + context 'on successful update' do + it 'sets the flash' do + go + expect(flash[:notice]).to eq 'Preferences saved.' + end + + it "changes the user's preferences" do + prefs = { + color_scheme_id: '1', + dashboard: 'stars', + theme_id: '2' + }.with_indifferent_access + + expect(user).to receive(:update_attributes).with(prefs) + + go params: prefs + end + end + + context 'on failed update' do + it 'sets the flash' do + expect(user).to receive(:update_attributes).and_return(false) + + go + + expect(flash[:alert]).to eq('Failed to save preferences.') + end + end + + context 'on invalid dashboard setting' do + it 'sets the flash' do + prefs = {dashboard: 'invalid'} + + go params: prefs + + expect(flash[:alert]).to match(/\AFailed to save preferences \(.+\)\.\z/) + end + end + + context 'as js' do + it 'renders' do + go + expect(response).to render_template :update + end + end + + context 'as html' do + it 'redirects' do + go format: :html + expect(response).to redirect_to(profile_preferences_path) + end + end + end +end diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb new file mode 100644 index 0000000000..abbbf6855f --- /dev/null +++ b/spec/controllers/root_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe RootController do + describe 'GET show' do + context 'with a user' do + let(:user) { create(:user) } + + before do + sign_in(user) + allow(subject).to receive(:current_user).and_return(user) + end + + context 'who has customized their dashboard setting' do + before do + user.update_attribute(:dashboard, 'stars') + end + + it 'redirects to their specified dashboard' do + get :show + expect(response).to redirect_to starred_dashboard_projects_path + end + end + + context 'who uses the default dashboard setting' do + it 'renders the default dashboard' do + get :show + expect(response).to render_template 'dashboard/show' + end + end + end + end +end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb new file mode 100644 index 0000000000..e1182e82f5 --- /dev/null +++ b/spec/features/profiles/preferences_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe 'Profile > Preferences' do + before do + login_as(:user) + visit profile_preferences_path + end + + describe 'User changes their application theme', js: true do + let(:default_class) { Gitlab::Theme.css_class_by_id(nil) } + let(:theme_5_class) { Gitlab::Theme.css_class_by_id(5) } + + it 'creates a flash message' do + choose 'user_theme_id_5' + + expect_preferences_saved_message + end + + it 'updates their preference' do + choose 'user_theme_id_5' + + allowing_for_delay do + visit page.current_path + expect(page).to have_checked_field("user_theme_id_5") + end + end + + it 'reflects the changes immediately' do + expect(page).to have_selector("body.#{default_class}") + + choose 'user_theme_id_5' + + expect(page).not_to have_selector("body.#{default_class}") + expect(page).to have_selector("body.#{theme_5_class}") + end + end + + describe 'User changes their syntax highlighting theme', js: true do + it 'creates a flash message' do + choose 'user_color_scheme_id_5' + + expect_preferences_saved_message + end + + it 'updates their preference' do + choose 'user_color_scheme_id_5' + + allowing_for_delay do + visit page.current_path + expect(page).to have_checked_field('user_color_scheme_id_5') + end + end + end + + describe 'User changes their default dashboard' do + it 'creates a flash message' do + select 'Starred Projects', from: 'user_dashboard' + click_button 'Save' + + expect_preferences_saved_message + end + + it 'updates their preference' do + select 'Starred Projects', from: 'user_dashboard' + click_button 'Save' + + click_link 'Dashboard' + expect(page.current_path).to eq starred_dashboard_projects_path + + click_link 'Your Projects' + expect(page.current_path).to eq dashboard_path + end + end + + def expect_preferences_saved_message + within('.flash-container') do + expect(page).to have_content('Preferences saved.') + end + end +end diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index 2512a9c0e3..2b09771851 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -36,8 +36,8 @@ describe "Profile access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /profile/design" do - subject { design_profile_path } + describe "GET /profile/preferences" do + subject { profile_preferences_path } it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb new file mode 100644 index 0000000000..0fba3ba563 --- /dev/null +++ b/spec/helpers/preferences_helper_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe PreferencesHelper do + describe 'dashboard_choices' do + it 'raises an exception when defined choices may be missing' do + expect(User).to receive(:dashboards).and_return(foo: 'foo') + expect { dashboard_choices }.to raise_error(RuntimeError) + end + + it 'raises an exception when defined choices may be using the wrong key' do + expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar') + expect { dashboard_choices }.to raise_error(KeyError) + end + + it 'provides better option descriptions' do + expect(dashboard_choices).to match_array [ + ['Your Projects (default)', 'projects'], + ['Starred Projects', 'stars'] + ] + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f1b8afa585..5bf6ce3536 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -50,12 +50,13 @@ # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) # location :string(255) +# public_email :string(255) default(""), not null # encrypted_otp_secret :string(255) # encrypted_otp_secret_iv :string(255) # encrypted_otp_secret_salt :string(255) # otp_required_for_login :boolean # otp_backup_codes :text -# public_email :string(255) default(""), not null +# dashboard :integer default(0) # require 'spec_helper' diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 953c8dd8dd..f268e4755d 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -102,7 +102,6 @@ end # profile_token GET /profile/token(.:format) profile#token # profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token # profile GET /profile(.:format) profile#show -# profile_design GET /profile/design(.:format) profile#design # profile_update PUT /profile/update(.:format) profile#update describe ProfilesController, "routing" do it "to #account" do @@ -120,9 +119,19 @@ describe ProfilesController, "routing" do it "to #show" do expect(get("/profile")).to route_to('profiles#show') end +end - it "to #design" do - expect(get("/profile/design")).to route_to('profiles#design') +# profile_preferences GET /profile/preferences(.:format) profiles/preferences#show +# PATCH /profile/preferences(.:format) profiles/preferences#update +# PUT /profile/preferences(.:format) profiles/preferences#update +describe Profiles::PreferencesController, 'routing' do + it 'to #show' do + expect(get('/profile/preferences')).to route_to('profiles/preferences#show') + end + + it 'to #update' do + expect(put('/profile/preferences')).to route_to('profiles/preferences#update') + expect(patch('/profile/preferences')).to route_to('profiles/preferences#update') end end @@ -195,11 +204,9 @@ end # dashboard GET /dashboard(.:format) dashboard#show # dashboard_issues GET /dashboard/issues(.:format) dashboard#issues # dashboard_merge_requests GET /dashboard/merge_requests(.:format) dashboard#merge_requests -# root / dashboard#show describe DashboardController, "routing" do it "to #index" do expect(get("/dashboard")).to route_to('dashboard#show') - expect(get("/")).to route_to('dashboard#show') end it "to #issues" do @@ -211,6 +218,14 @@ describe DashboardController, "routing" do end end +# root / root#show +describe RootController, 'routing' do + it 'to #show' do + expect(get('/')).to route_to('root#show') + end +end + + # new_user_session GET /users/sign_in(.:format) devise/sessions#new # user_session POST /users/sign_in(.:format) devise/sessions#create # destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index fed1ab6ee3..3e41aec425 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -19,3 +19,36 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +module CapybaraHelpers + # Execute a block a certain number of times before considering it a failure + # + # The given block is called, and if it raises a `Capybara::ExpectationNotMet` + # error, we wait `interval` seconds and then try again, until `retries` is + # met. + # + # This allows for better handling of timing-sensitive expectations in a + # sketchy CI environment, for example. + # + # interval - Delay between retries in seconds (default: 0.5) + # retries - Number of times to execute before failing (default: 5) + def allowing_for_delay(interval: 0.5, retries: 5) + tries = 0 + + begin + yield + rescue Capybara::ExpectationNotMet => ex + if tries <= retries + tries += 1 + sleep interval + retry + else + raise ex + end + end + end +end + +RSpec.configure do |config| + config.include CapybaraHelpers, type: :feature +end