Merge branch 'master' into 3296-user-profile
authorradhika <radhika@curoverse.com>
Tue, 12 Aug 2014 19:10:46 +0000 (15:10 -0400)
committerradhika <radhika@curoverse.com>
Tue, 12 Aug 2014 19:10:46 +0000 (15:10 -0400)
16 files changed:
apps/workbench/app/assets/stylesheets/application.css.scss
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/user_agreements_controller.rb
apps/workbench/app/models/user.rb
apps/workbench/app/views/layouts/body.html.erb
apps/workbench/app/views/users/profile.html.erb [new file with mode: 0644]
apps/workbench/config/application.default.yml
apps/workbench/config/routes.rb
apps/workbench/test/integration/application_layout_test.rb [new file with mode: 0644]
services/api/app/mailers/profile_notifier.rb [new file with mode: 0644]
services/api/app/models/user.rb
services/api/app/views/profile_notifier/profile_created.text.erb [new file with mode: 0644]
services/api/config/application.default.yml
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/users.yml
services/api/test/functional/arvados/v1/users_controller_test.rb

index c986f034bcc6a0e19f8525659201a3fe8f6cb74c..6b91783e60abe882bbe7b5ba06fb33a4979e790f 100644 (file)
@@ -244,3 +244,7 @@ div.pane-content iframe {
   width: 100%;
   border: none;
 }
+
+div.rounded {
+  border-radius: 3px;
+}
index 10a1de8d4f3ea212bd7ce5436025bf0dd44b6c50..222888085d9e3bb95973f2a2865c3efbb569b7f5 100644 (file)
@@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
   around_filter :require_thread_api_token, except: ERROR_ACTIONS
   before_filter :accept_uuid_as_id_param, except: ERROR_ACTIONS
   before_filter :check_user_agreements, except: ERROR_ACTIONS
+  before_filter :check_user_profile, except: [:update_profile] + ERROR_ACTIONS
   before_filter :check_user_notifications, except: ERROR_ACTIONS
   before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
   before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS
@@ -409,9 +410,6 @@ class ApplicationController < ActionController::Base
     Thread.current[:arvados_api_token] = new_token
     if new_token.nil?
       Thread.current[:user] = nil
-    elsif (new_token == session[:arvados_api_token]) and
-        session[:user].andand[:is_active]
-      Thread.current[:user] = User.new(session[:user])
     else
       Thread.current[:user] = User.current
     end
@@ -429,15 +427,7 @@ class ApplicationController < ActionController::Base
       false  # We may redirect to login, or not, based on the current action.
     else
       session[:arvados_api_token] = params[:api_token]
-      session[:user] = {
-        uuid: user.uuid,
-        email: user.email,
-        first_name: user.first_name,
-        last_name: user.last_name,
-        is_active: user.is_active,
-        is_admin: user.is_admin,
-        prefs: user.prefs
-      }
+
       if !request.format.json? and request.method.in? ['GET', 'HEAD']
         # Repeat this request with api_token in the (new) session
         # cookie instead of the query string.  This prevents API
@@ -531,6 +521,41 @@ class ApplicationController < ActionController::Base
     true
   end
 
+  def check_user_profile
+    if request.method.downcase != 'get' || params[:partial] ||
+       params[:tab_pane] || params[:action_method] ||
+       params[:action] == 'setup_popup'
+      return true
+    end
+
+    if missing_required_profile?
+      render 'users/profile'
+    end
+    true
+  end
+
+  helper_method :missing_required_profile?
+  def missing_required_profile?
+    missing_required = false
+
+    profile_config = Rails.configuration.user_profile_form_fields
+    if current_user && profile_config
+      current_user_profile = current_user.prefs[:profile]
+      profile_config.kind_of?(Array) && profile_config.andand.each do |entry|
+        if entry['required']
+          if !current_user_profile ||
+             !current_user_profile[entry['key'].to_sym] ||
+             current_user_profile[entry['key'].to_sym].empty?
+            missing_required = true
+            break
+          end
+        end
+      end
+    end
+
+    missing_required
+  end
+
   def select_theme
     return Rails.configuration.arvados_theme
   end
index 6ab8ae215f488888b04bcb2362f52596e26603f7..924bf44baeee801735d905a431743bddd49e0ff8 100644 (file)
@@ -1,6 +1,7 @@
 class UserAgreementsController < ApplicationController
   skip_before_filter :check_user_agreements
   skip_before_filter :find_object_by_uuid
+  skip_before_filter :check_user_profile
 
   def model_class
     Collection
index af1922adffa5fea605fca8b83edda1c73d0db45d..87ea5faefa85976b8d11350b291dffc28b2514d7 100644 (file)
@@ -53,4 +53,10 @@ class User < ArvadosBase
     arvados_api_client.api(self, "/setup", params)
   end
 
+  def update_profile params
+    self.private_reload(arvados_api_client.api(self.class,
+                                               "/#{self.uuid}/profile",
+                                               params))
+  end
+
 end
index a8c45847474432ab597a29c2e8b38163e125779c..9282acdfe5a8d6db59edb370fa5048e8a1c22d12 100644 (file)
               <%= current_user.email %>
             </a>
             <ul class="dropdown-menu" role="menu">
+              <li role="presentation" class="dropdown-header">
+                My account
+              </li>
               <% if current_user.is_active %>
               <li role="presentation"><a href="/manage_account" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage account</a></li>
+              <% if Rails.configuration.user_profile_form_fields %>
+                <li role="presentation"><a href="/users/<%=current_user.uuid%>/profile" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage my profile</a></li>
+              <% end %>
               <li role="presentation" class="divider"></li>
               <% end %>
               <li role="presentation"><a href="<%= logout_path %>" role="menuitem"><i class="fa fa-sign-out fa-fw"></i> Log out</a></li>
diff --git a/apps/workbench/app/views/users/profile.html.erb b/apps/workbench/app/views/users/profile.html.erb
new file mode 100644 (file)
index 0000000..2c40fef
--- /dev/null
@@ -0,0 +1,108 @@
+<%
+    profile_config = Rails.configuration.user_profile_form_fields
+    current_user_profile = current_user.prefs[:profile]
+    show_save_button = false
+
+    profile_message = Rails.configuration.user_profile_form_message ? Rails.configuration.user_profile_form_message : 'You can manage your profile using this page. Any feilds in red are required and missing. Please fill in those fields before you can accesse Arvados Workbench.'
+
+    missing_required = missing_required_profile?
+
+    profile_url = '/users/'+current_user.uuid+'/profile'
+    target = request.url.partition('?target=')[-1]
+    target = request.url if target.empty?
+    return_to_url = (request.url.ends_with? profile_url) ? profile_url : profile_url+'?target='+target
+%>
+
+<div>
+    <div class="panel panel-default">
+        <div class="panel-heading">
+          <h4 class="panel-title">
+            Profile
+          </h4>
+        </div>
+        <div class="panel-body">
+          <% if !missing_required && params.andand.keys.include?('target') %>
+            <div class="rounded" style="border-width: 1px; border-style: dotted; border-color: lightgray;">
+              <p style="margin: 8px;">Thank you for filling in your profile. If you are done updating your profile,
+                 you can now access Arvados Workbench by clicking on this button.
+                  <form action="<%=target%>">
+                    <input style="margin-left: 8px;" class="btn btn-primary" type="submit" value="Access Arvados Workbench">
+                  </form>
+              </p>
+            </div>
+          <% else %>
+            <div class="rounded" style="border-width: 1px; border-style: dotted; border-color: lightgray;">
+              <p style="margin: 8px;"> <%=raw(profile_message)%> </p>
+            </div>
+          <% end %>
+
+          <div class="rounded" style="border-width: 1px; border-style: dotted; border-color: lightgray;">
+            <%= form_tag "/users/#{current_user.uuid}", {method: 'patch', id: 'save_profile_form', name: 'save_profile_form', class: 'form-horizontal'} do %>
+              <%= hidden_field_tag :return_to, return_to_url %>
+              <div class="form-group">
+                  <label for="email" class="col-sm-3 control-label"> Email </label>
+                  <div class="col-sm-8">
+                    <p class="form-control-static" id="email" name="email"><%=current_user.email%></p>
+                  </div>
+              </div>
+              <div class="form-group">
+                  <label for="first_name" class="col-sm-3 control-label"> First name </label>
+                  <div class="col-sm-8">
+                    <p class="form-control-static" id="first_name" name="first_name"><%=current_user.first_name%></p>
+                  </div>
+              </div>
+              <div class="form-group">
+                  <label for="last_name" class="col-sm-3 control-label"> Last name </label>
+                  <div class="col-sm-8">
+                    <p class="form-control-static" id="last_name" name="last_name"><%=current_user.last_name%></p>
+                  </div>
+              </div>
+              <div class="form-group">
+                  <label for="identity_url" class="col-sm-3 control-label"> Identity URL </label>
+                  <div class="col-sm-8">
+                    <p class="form-control-static" id="identity_url" name="identity_url"><%=current_user.andand.identity_url%></p>
+                  </div>
+              </div>
+
+              <% profile_config.kind_of?(Array) && profile_config.andand.each do |entry| %>
+                <% if entry['key'] %>
+                  <%
+                      show_save_button = true
+                      label = entry['required'] ? '* ' : ''
+                      label += entry['form_field_title']
+                      value = current_user_profile[entry['key'].to_sym] if current_user_profile
+                  %>
+                  <div class="form-group">
+                    <label for="<%=entry['key']%>"
+                           class="col-sm-3 control-label"
+                           style=<%="color:red" if entry['required']&&(!value||value.empty?)%>> <%=label%>
+                    </label>
+                    <% if entry['type'] == 'select' %>
+                      <div class="col-sm-8">
+                        <select class="form-control" name="user[prefs][:profile][:<%=entry['key']%>]">
+                          <% entry['options'].each do |option| %>
+                            <option value="<%=option%>" <%='selected' if option==value%>><%=option%></option>
+                          <% end %>
+                        </select>
+                      </div>
+                    <% else %>
+                      <div class="col-sm-8">
+                        <input type="text" class="form-control" name="user[prefs][:profile][:<%=entry['key']%>]" placeholder="<%=entry['form_field_description']%>" value="<%=value%>" ></input>
+                      </div>
+                    <% end %>
+                  </div>
+                <% end %>
+              <% end %>
+
+              <% if show_save_button %>
+                <div class="form-group">
+                  <div class="col-sm-offset-3 col-sm-8">
+                    <button type="submit" class="btn btn-primary">Save profile</button>
+                  </div>
+                </div>
+              <% end %>
+            <% end %>
+          </div>
+        </div>
+    </div>
+</div>
index 2fe701afcb6d5efdd9b92abba0ef94f571f23058..3b4c2c0684bdb6cff3795776bc60b5588d2ec3ec 100644 (file)
@@ -58,6 +58,25 @@ test:
 
   site_name: Workbench:test
 
+  # Enable user profile with one required field
+  user_profile_form_fields:
+    - key: organization
+      type: text
+      form_field_title: Institution
+      form_field_description: Your organization
+      required: true
+    - key: role
+      type: select
+      form_field_title: Your role
+      form_field_description: Choose the category that best describes your role in your organization.
+      options:
+        - Bio-informatician
+        - Computational biologist
+        - Biologist or geneticist
+        - Software developer
+        - IT
+        - Other
+
 common:
   assets.js_compressor: false
   assets.css_compressor: false
@@ -74,3 +93,41 @@ common:
   secret_key_base: false
   default_openid_prefix: https://www.google.com/accounts/o8/id
   send_user_setup_notification_email: true
+
+  # Set user_profile_form_fields to enable and configure the user profile page.
+  # Default is set to false. A commented setting with full description is provided below.
+  user_profile_form_fields: false
+
+  # Below is a sample setting of user_profile_form_fields config parameter.
+  # This configuration parameter should be set to either false (to disable) or
+  # to an array as shown below. 
+  # Configure the list of input fields to be displayed in the profile page
+  # using the attribute "key" for each of the input fields.
+  # This sample shows configuration with one required and one optional form fields.
+  # For each of these input fields:
+  #   You can specify "type" as "text" or "select".
+  #   List the "options" to be displayed for each of the "select" menu.
+  #   Set "required" as "true" for any of these fields to make them required.
+  # If any of the required fields are missing in the user's profile, the user will be
+  # redirected to the profile page before they can access any Workbench features.
+  #user_profile_form_fields:
+  #  - key: organization
+  #    type: text
+  #    form_field_title: Institution/Company
+  #    form_field_description: Your organization
+  #    required: true
+  #  - key: role
+  #    type: select
+  #    form_field_title: Your role
+  #    form_field_description: Choose the category that best describes your role in your organization.
+  #    options:
+  #      - Bio-informatician
+  #      - Computational biologist
+  #      - Biologist or geneticist
+  #      - Software developer
+  #      - IT
+  #      - Other
+
+  # Use "user_profile_form_message" to configure the message you want to display in
+  # the profile page. If this is not provided, a default message will be displayed.
+  user_profile_form_message: Welcome to Arvados. Please fill in all required fields before you can access Arvados Workbench. Missing required fields are in <span style="color:red">red</span>.
index 091a06987174697ea5f81b0d36b293173ab44801..4ef26612ab194a2df62eefa3945aa1ab75ab5c4e 100644 (file)
@@ -32,6 +32,7 @@ ArvadosWorkbench::Application.routes.draw do
     post 'sudo', :on => :member
     post 'unsetup', :on => :member
     get 'setup_popup', :on => :member
+    get 'profile', :on => :member
   end
   get '/manage_account' => 'users#manage_account'
   get "/add_ssh_key_popup" => 'users#add_ssh_key_popup', :as => :add_ssh_key_popup
diff --git a/apps/workbench/test/integration/application_layout_test.rb b/apps/workbench/test/integration/application_layout_test.rb
new file mode 100644 (file)
index 0000000..f231e4d
--- /dev/null
@@ -0,0 +1,350 @@
+require 'integration_helper'
+require 'selenium-webdriver'
+require 'headless'
+
+class ApplicationLayoutTest < ActionDispatch::IntegrationTest
+  setup do
+    headless = Headless.new
+    headless.start
+    Capybara.current_driver = :selenium
+
+    @user_profile_form_fields = Rails.configuration.user_profile_form_fields
+  end
+
+  teardown do
+    Rails.configuration.user_profile_form_fields = @user_profile_form_fields
+  end
+
+  def verify_homepage_with_profile user, invited, has_profile
+    profile_config = Rails.configuration.user_profile_form_fields
+
+    if !user
+      assert page.has_text? 'Please log in'
+      assert page.has_text? 'The "Log in" button below will show you a Google sign-in page'
+      assert page.has_no_text? 'My projects'
+      assert page.has_link? "Log in to #{Rails.configuration.site_name}"
+    elsif profile_config && !has_profile && user['is_active']
+      add_profile user
+    elsif user['is_active']
+      assert page.has_text? 'My projects'
+      assert page.has_text? 'Projects shared with me'
+      assert page.has_no_text? 'Save profile'
+    elsif invited
+      assert page.has_text? 'Please check the box below to indicate that you have read and accepted the user agreement'
+      assert page.has_no_text? 'Save profile'
+    else
+      assert page.has_text? 'Your account is inactive'
+      assert page.has_no_text? 'Save profile'
+    end
+
+    within('.navbar-fixed-top') do
+      if !user
+        assert page.has_link? 'Log in'
+      else
+        # my account menu
+        assert page.has_link? "#{user['email']}"
+        find('a', text: "#{user['email']}").click
+        within('.dropdown-menu') do
+          if !invited
+            page.has_no_link? ('Not active')
+          else
+            page.has_no_link? ('Sign agreements')
+            page.has_link? ('Manage account')
+
+            if profile_config
+              page.has_link? ('Manage profile')
+            else
+              page.has_no_link? ('Manage profile')
+            end
+          end
+          page.has_link? ('Log out')
+        end
+      end
+    end
+  end
+
+  # test the help menu
+  def check_help_menu
+    within('.navbar-fixed-top') do
+      page.find("#arv-help").click
+      within('.dropdown-menu') do
+        assert page.has_link? 'Tutorials and User guide'
+        assert page.has_link? 'API Reference'
+        assert page.has_link? 'SDK Reference'
+      end
+    end
+  end
+
+  def verify_system_menu user
+    if user && user['is_active']
+      look_for_add_new = nil
+      within('.navbar-fixed-top') do
+        page.find("#system-menu").click
+        if user['is_admin']
+          within('.dropdown-menu') do
+            assert page.has_text? 'Groups'
+            assert page.has_link? 'Repositories'
+            assert page.has_link? 'Virtual machines'
+            assert page.has_link? 'SSH keys'
+            assert page.has_link? 'API tokens'
+            find('a', text: 'Users').click
+            look_for_add_new = 'Add a new user'
+          end
+        else
+          within('.dropdown-menu') do
+            assert page.has_no_text? 'Users'
+            assert page.has_no_link? 'Repositories'
+            assert page.has_no_link? 'Virtual machines'
+            assert page.has_no_link? 'SSH keys'
+            assert page.has_no_link? 'API tokens'
+
+            find('a', text: 'Groups').click
+            look_for_add_new = 'Add a new group'
+          end
+        end
+      end
+      if look_for_add_new
+        assert page.has_text? look_for_add_new
+      end
+    else
+      assert page.has_no_link? '#system-menu'
+    end
+  end
+
+  # test manage_account page
+  def verify_manage_account user
+    if user && user['is_active']
+      within('.navbar-fixed-top') do
+        find('a', text: "#{user['email']}").click
+        within('.dropdown-menu') do
+          find('a', text: 'Manage account').click
+        end
+      end
+
+      # now in manage account page
+      assert page.has_text? 'Virtual Machines'
+      assert page.has_text? 'Repositories'
+      assert page.has_text? 'SSH Keys'
+      assert page.has_text? 'Current Token'
+
+      assert page.has_text? 'The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados'
+
+      click_link 'Add new SSH key'
+
+      within '.modal-content' do
+        assert page.has_text? 'Public Key'
+        assert page.has_button? 'Cancel'
+        assert page.has_button? 'Submit'
+
+        page.find_field('public_key').set 'first test with an incorrect ssh key value'
+        click_button 'Submit'
+        assert page.has_text? 'Public key does not appear to be a valid ssh-rsa or dsa public key'
+
+        public_key_str = api_fixture('authorized_keys')['active']['public_key']
+        page.find_field('public_key').set public_key_str
+        page.find_field('name').set 'added_in_test'
+        click_button 'Submit'
+        assert page.has_text? 'Public key already exists in the database, use a different key.'
+
+        new_key = SSHKey.generate
+        page.find_field('public_key').set new_key.ssh_public_key
+        page.find_field('name').set 'added_in_test'
+        click_button 'Submit'
+      end
+    end
+
+    # key must be added. look for it in the refreshed page
+    assert page.has_text? 'added_in_test'
+  end
+
+  # Check manage profile page and add missing profile to the user
+  def add_profile user
+    assert page.has_no_text? 'My projects'
+    assert page.has_no_text? 'Projects shared with me'
+
+    assert page.has_text? 'Profile'
+    assert page.has_text? 'First name'
+    assert page.has_text? 'Last name'
+    assert page.has_text? 'Identity URL'
+    assert page.has_text? 'Email'
+    assert page.has_text? user['email']
+
+    # Using the default profile which has message and one required field
+
+    # Save profile without filling in the required field. Expect to be back in this profile page again
+    click_button "Save profile"
+    assert page.has_text? 'Profile'
+    assert page.has_text? 'First name'
+    assert page.has_text? 'Last name'
+    assert page.has_text? 'Save profile'
+
+    # This time fill in required field and then save. Expect to go to requested page after that.
+    profile_message = Rails.configuration.user_profile_form_message
+    required_field_title = ''
+    required_field_key = ''
+    profile_config = Rails.configuration.user_profile_form_fields
+    profile_config.andand.each do |entry|
+      if entry['required']
+        required_field_key = entry['key']
+        required_field_title = entry['form_field_title']
+      end
+    end
+
+    assert page.has_text? profile_message[0,25]
+    assert page.has_text? required_field_title
+    page.find_field('user[prefs][:profile][:'+required_field_key+']').set 'value to fill required field'
+
+    click_button "Save profile"
+    # profile saved and in profile page now with success
+    assert page.has_text? 'Thank you for filling in your profile'
+    click_button 'Access Arvados Workbench'
+
+    # profile saved and in home page now
+    assert page.has_text? 'My projects'
+    assert page.has_text? 'Projects shared with me'
+  end
+
+  # test the search box
+  def verify_search_box user
+    if user && user['is_active']
+      # let's search for a valid uuid
+      within('.navbar-fixed-top') do
+        page.find_field('search').set user['uuid']
+        page.find('.glyphicon-search').click
+      end
+
+      # we should now be in the user's page as a result of search
+      assert page.has_text? user['first_name']
+
+      # let's search again for an invalid valid uuid
+      within('.navbar-fixed-top') do
+        search_for = String.new user['uuid']
+        search_for[0]='1'
+        page.find_field('search').set search_for
+        page.find('.glyphicon-search').click
+      end
+
+      # we should see 'not found' error page
+      assert page.has_text? 'Not Found'
+
+      # let's search for the anonymously accessible project
+      publicly_accessible_project = api_fixture('groups')['anonymously_accessible_project']
+
+      within('.navbar-fixed-top') do
+        # search again for the anonymously accessible project
+        page.find_field('search').set publicly_accessible_project['name'][0,10]
+        page.find('.glyphicon-search').click
+      end
+
+      within '.modal-content' do
+        assert page.has_text? 'All projects'
+        assert page.has_text? 'Search'
+        assert page.has_text? 'Cancel'
+        assert_selector('div', text: publicly_accessible_project['name'])
+        find(:xpath, '//div[./span[contains(.,publicly_accessible_project["uuid"])]]').click
+
+        click_button 'Show'
+      end
+
+      # seeing "Unrestricted public data" now
+      assert page.has_text? publicly_accessible_project['name']
+      assert page.has_text? publicly_accessible_project['description']
+    end
+  end
+
+  [
+    [nil, nil, false, false],
+    ['inactive', api_fixture('users')['inactive'], true, false],
+    ['inactive_uninvited', api_fixture('users')['inactive_uninvited'], false, false],
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+    ['active_no_prefs', api_fixture('users')['active_no_prefs'], true, false],
+    ['active_no_prefs_profile', api_fixture('users')['active_no_prefs_profile'], true, false],
+  ].each do |token, user, invited, has_profile|
+      test "visit home page when profile is configured for user #{token}" do
+      # Our test config enabled profile by default. So, no need to update config
+      if !token
+        visit ('/')
+      else
+        visit page_with_token(token)
+      end
+
+      verify_homepage_with_profile user, invited, has_profile
+    end
+  end
+
+  [
+    [nil, nil, false, false],
+    ['inactive', api_fixture('users')['inactive'], true, false],
+    ['inactive_uninvited', api_fixture('users')['inactive_uninvited'], false, false],
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+    ['active_no_prefs', api_fixture('users')['active_no_prefs'], true, false],
+    ['active_no_prefs_profile', api_fixture('users')['active_no_prefs_profile'], true, false],
+  ].each do |token, user, invited, has_profile|
+    test "visit home page when profile not configured for user #{token}" do
+      Rails.configuration.user_profile_form_fields = false
+
+      if !token
+        visit ('/')
+      else
+        visit page_with_token(token)
+      end
+
+      verify_homepage_with_profile user, invited, has_profile
+    end
+  end
+
+  [
+    [nil, nil, false, false],
+    ['inactive', api_fixture('users')['inactive'], true, false],
+    ['inactive_uninvited', api_fixture('users')['inactive_uninvited'], false, false],
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+    ['active_no_prefs', api_fixture('users')['active_no_prefs'], true, false],
+    ['active_no_prefs_profile', api_fixture('users')['active_no_prefs_profile'], true, false],
+  ].each do |token, user, invited, has_profile|
+    test "check help for user #{token}" do
+      Rails.configuration.user_profile_form_fields = false
+
+      if !token
+        visit ('/')
+      else
+        visit page_with_token(token)
+      end
+
+      check_help_menu
+    end
+  end
+
+  [
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+  ].each do |token, user|
+    test "test system menu for user #{token}" do
+      visit page_with_token(token)
+      verify_system_menu user
+    end
+  end
+
+  [
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+  ].each do |token, user|
+    test "test manage account for user #{token}" do
+      visit page_with_token(token)
+      verify_manage_account user
+    end
+  end
+
+  [
+    ['active', api_fixture('users')['active'], true, true],
+    ['admin', api_fixture('users')['admin'], true, true],
+  ].each do |token, user|
+    test "test search for user #{token}" do
+      visit page_with_token(token)
+      verify_search_box user
+    end
+  end
+
+end
diff --git a/services/api/app/mailers/profile_notifier.rb b/services/api/app/mailers/profile_notifier.rb
new file mode 100644 (file)
index 0000000..13e3b34
--- /dev/null
@@ -0,0 +1,8 @@
+class ProfileNotifier < ActionMailer::Base
+  default from: Rails.configuration.admin_notifier_email_from
+
+  def profile_created(user, address)
+    @user = user
+    mail(to: address, subject: "Profile created by #{@user.email}")
+  end
+end
index 4fc6204eb504cf941e14abba0d15113dd1935ccc..64e0d09451992e8b43af7348a1b0d04c3cdcc212 100644 (file)
@@ -13,6 +13,8 @@ class User < ArvadosModel
   before_create :check_auto_admin
   after_create :add_system_group_permission_link
   after_create :send_admin_notifications
+  after_update :send_profile_created_notification
+
 
   has_many :authorized_keys, :foreign_key => :authorized_user_uuid, :primary_key => :uuid
 
@@ -429,4 +431,15 @@ class User < ArvadosModel
       AdminNotifier.new_inactive_user(self).deliver
     end
   end
+
+  # Send notification if the user saved profile for the first time
+  def send_profile_created_notification
+    if self.prefs_changed?
+      if self.prefs_was.andand.empty? || !self.prefs_was.andand['profile']
+        profile_notification_address = Rails.configuration.user_profile_notification_address
+        ProfileNotifier.profile_created(self, profile_notification_address).deliver if profile_notification_address
+      end
+    end
+  end
+
 end
diff --git a/services/api/app/views/profile_notifier/profile_created.text.erb b/services/api/app/views/profile_notifier/profile_created.text.erb
new file mode 100644 (file)
index 0000000..73adf28
--- /dev/null
@@ -0,0 +1,2 @@
+Profile created by user <%=@user.full_name%> <%=@user.email%>
+User's profile: <%=@user.prefs['profile']%>
index c32900cfaf01e73f9ab7cd542547020233528360..ddcaa57302b5ceec5437ab1b20cb09271b2f24ea 100644 (file)
@@ -43,6 +43,9 @@ test:
   secret_token: <%= rand(2**512).to_s(36) %>
   blob_signing_key: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
 
+  # email address to which mail should be sent when the user creates profile for the first time
+  user_profile_notification_address: arvados@example.com
+
 common:
   uuid_prefix: <%= Digest::MD5.hexdigest(`hostname`).to_i(16).to_s(36)[0..4] %>
 
@@ -173,3 +176,6 @@ common:
   # to sign session tokens. IMPORTANT: This is a site secret. It
   # should be at least 50 characters.
   secret_token: ~
+
+  # email address to which mail should be sent when the user creates profile for the first time
+  user_profile_notification_address: false
index 4fa41621d7b9b8f300c38b117c0fa345b9e9a40f..c0bce93daa9e8fe83c784ad0d10a9365625e6953 100644 (file)
@@ -162,3 +162,15 @@ job_reader:
   user: job_reader
   api_token: e99512cdc0f3415c2428b9758f33bdfb07bc3561b00e86e7e6
   expires_at: 2038-01-01 00:00:00
+
+active_no_prefs:
+  api_client: untrusted
+  user: active_no_prefs
+  api_token: 3kg612cdc0f3415c2428b9758f33bdfb07bc3561b00e86qdmi
+  expires_at: 2038-01-01 00:00:00
+
+active_no_prefs_profile:
+  api_client: untrusted
+  user: active_no_prefs_profile
+  api_token: 3kg612cdc0f3415c242856758f33bdfb07bc3561b00e86qdmi
+  expires_at: 2038-01-01 00:00:00
index 0e02b7d8f1f638d7e4397c67e2b40c18f2566251..5c688363181c2bb19ab740bb5cff6305e53260c8 100644 (file)
@@ -9,7 +9,10 @@ admin:
   identity_url: https://admin.openid.local
   is_active: true
   is_admin: true
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: IT
 
 miniadmin:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -20,7 +23,10 @@ miniadmin:
   identity_url: https://miniadmin.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: IT
 
 rominiadmin:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -31,7 +37,10 @@ rominiadmin:
   identity_url: https://rominiadmin.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: IT
 
 active:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -42,7 +51,10 @@ active:
   identity_url: https://active-user.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
 
 project_viewer:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -53,7 +65,10 @@ project_viewer:
   identity_url: https://project-viewer.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
 
 future_project_user:
   # Workbench tests give this user permission on aproject.
@@ -64,7 +79,10 @@ future_project_user:
   identity_url: https://future-project-user.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
 
 spectator:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -75,7 +93,10 @@ spectator:
   identity_url: https://spectator.openid.local
   is_active: true
   is_admin: false
-  prefs: {}
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
 
 inactive_uninvited:
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -129,4 +150,30 @@ job_reader:
   identity_url: https://spectator.openid.local
   is_active: true
   is_admin: false
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+
+active_no_prefs:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-a46c42d1td4aoj4
+  email: active_no_prefs@arvados.local
+  first_name: NoPrefs
+  last_name: NoProfile
+  identity_url: https://active_no_prefs.openid.local
+  is_active: true
+  is_admin: false
   prefs: {}
+
+active_no_prefs_profile:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-a46c98d1td4aoj4
+  email: active_no_prefs_profile@arvados.local
+  first_name: HasPrefs
+  last_name: NoProfile
+  identity_url: https://active_no_prefs_profile.openid.local
+  is_active: true
+  is_admin: false
+  prefs:
+    test: abc
index 28367831e18a48c9b954355626624905fb67eda8..a448d1a4bdcf3d90df109c863474a105cf48ea9a 100644 (file)
@@ -842,6 +842,71 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     check_active_users_index
   end
 
+  test "update active_no_prefs user profile and expect notification email" do
+    authorize_with :admin
+
+    put :update, {
+      id: users(:active_no_prefs).uuid,
+      user: {
+        prefs: {:profile => {'organization' => 'example.com'}}
+      }
+    }
+    assert_response :success
+
+    found_email = false
+    ActionMailer::Base.deliveries.andand.each do |email|
+      if email.subject == "Profile created by #{users(:active_no_prefs).email}"
+        found_email = true
+        break
+      end
+    end
+    assert_equal true, found_email, 'Expected email after creating profile'
+  end
+
+  test "update active_no_prefs_profile user profile and expect notification email" do
+    authorize_with :admin
+
+    user = {}
+    user[:prefs] = users(:active_no_prefs_profile).prefs
+    user[:prefs][:profile] = {:profile => {'organization' => 'example.com'}}
+    put :update, {
+      id: users(:active_no_prefs_profile).uuid,
+      user: user
+    }
+    assert_response :success
+
+    found_email = false
+    ActionMailer::Base.deliveries.andand.each do |email|
+      if email.subject == "Profile created by #{users(:active_no_prefs_profile).email}"
+        found_email = true
+        break
+      end
+    end
+    assert_equal true, found_email, 'Expected email after creating profile'
+  end
+
+  test "update active user profile and expect no notification email" do
+    authorize_with :admin
+
+    put :update, {
+      id: users(:active).uuid,
+      user: {
+        prefs: {:profile => {'organization' => 'anotherexample.com'}}
+      }
+    }
+    assert_response :success
+
+    found_email = false
+    ActionMailer::Base.deliveries.andand.each do |email|
+      if email.subject == "Profile created by #{users(:active).email}"
+        found_email = true
+        break
+      end
+    end
+    assert_equal false, found_email, 'Expected no email after updating profile'
+  end
+
+
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
                          "last_name"].sort