+ + Detailed export is only supported from the individual Case view. +
CSV file withTeam Name,Case Name,Case ID,Query Text,Doc ID,Title,Rating,Field1,...,FieldN
where Field1,...,FieldN
are specified under Settings in the Displayed Fields field.
@@ -74,5 +78,5 @@ - Read more about the Quepid story. Contact us - to talk about Quepid or ask how we can help with your search problems! -
-wt=json
.
+ If you are using a version of Solr between 8.4 and 9.0, you may need to tweak the content-type being set by wt=json
.
You can do this via the below curl command:
curl -X POST -H 'Content-type:application/json' -d '{ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 000000000..9aec23053 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 000000000..8d6c2a1bf --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 769013881..8b4a191e4 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class AccountsController < ApplicationController - force_ssl if: :ssl_enabled? - # rubocop:disable Metrics/MethodLength def update @user = current_user @@ -41,7 +39,7 @@ def destroy respond_to do |format| if @user.destroy - format.html { redirect_to secure_url, notice: 'Account was deleted' } + format.html { redirect_to sessions_path, notice: 'Account was deleted' } format.json { head :no_content } else format.html { render 'profiles/show' } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f8835c6df..06065b78b 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -70,11 +70,13 @@ def set_user @user = User.find(params[:id]) end - # Never trust parameters from the scary internet, only allow the white list through. + # Never trust parameters from the scary internet, only allow the permitted list through. def user_params params.require(:user).permit( :administrator, - :email + :email, + :password, + :password_confirmation ) end end diff --git a/app/controllers/api/v1/cases/dropdown_controller.rb b/app/controllers/api/v1/cases/dropdown_controller.rb index 90fe85777..8ff92168b 100644 --- a/app/controllers/api/v1/cases/dropdown_controller.rb +++ b/app/controllers/api/v1/cases/dropdown_controller.rb @@ -19,7 +19,7 @@ def index LEFT OUTER JOIN `teams` ON `teams`.`id` = `teams_cases`.`team_id` LEFT OUTER JOIN `teams_members` ON `teams_members`.`team_id` = `teams`.`id` LEFT OUTER JOIN `users` ON `users`.`id` = `teams_members`.`member_id` - WHERE (`teams_members`.`member_id` = #{current_user.id} OR `cases`.`user_id` = #{current_user.id}) + WHERE (`teams_members`.`member_id` = #{current_user.id} OR `cases`.`owner_id` = #{current_user.id}) AND (`cases`.`archived` = false OR `cases`.`archived` IS NULL) ORDER BY `case_metadata`.`last_viewed_at` DESC, `cases`.`id` DESC LIMIT 3 diff --git a/app/controllers/api/v1/cases_controller.rb b/app/controllers/api/v1/cases_controller.rb index be29327b8..0f19f795f 100644 --- a/app/controllers/api/v1/cases_controller.rb +++ b/app/controllers/api/v1/cases_controller.rb @@ -20,7 +20,7 @@ def index if archived @no_tries = true @no_teams = true - @cases = Case.where(archived: archived, user_id: current_user.id).all + @cases = Case.where(archived: archived, owner_id: current_user.id).all else @cases = if 'last_viewed_at' == sort_by current_user.cases_involved_with.not_archived.includes(:metadata).references(:metadata) diff --git a/app/controllers/api/v1/export/ratings_controller.rb b/app/controllers/api/v1/export/ratings_controller.rb index 26c2b2cee..f43f2d273 100644 --- a/app/controllers/api/v1/export/ratings_controller.rb +++ b/app/controllers/api/v1/export/ratings_controller.rb @@ -27,9 +27,9 @@ def show respond_to do |format| format.json do - json_template = file_format.nil? ? 'show.json.jbuilder' : "show.#{file_format.downcase}.json.jbuilder" + json_template = file_format.nil? ? 'show' : "show_#{file_format.downcase}" - render json_template + render json_template, formats: :json end format.csv do # We have crazy rendering formatting in the view because we don't want a trailing LF at the end of the diff --git a/app/controllers/api/v1/import/ratings_controller.rb b/app/controllers/api/v1/import/ratings_controller.rb index f28b5774e..cc5761732 100644 --- a/app/controllers/api/v1/import/ratings_controller.rb +++ b/app/controllers/api/v1/import/ratings_controller.rb @@ -81,7 +81,7 @@ def create rescue Exception => e # TODO: report this to logging infrastructure so we won't lose any important # errors that we might have to fix. - Rails.logger.debug "Import ratings failed: #{e.inspect}" + Rails.logger.debug { "Import ratings failed: #{e.inspect}" } render json: { message: e.message }, status: :bad_request end diff --git a/app/controllers/api/v1/teams_controller.rb b/app/controllers/api/v1/teams_controller.rb index 9ffd2116e..13971df39 100644 --- a/app/controllers/api/v1/teams_controller.rb +++ b/app/controllers/api/v1/teams_controller.rb @@ -12,7 +12,7 @@ def index # @teams = current_user.teams_im_in # @teams = @teams.preload(:scorers, :members, :cases, :owner).all # There may be some more fields we could include... - @teams = current_user.teams.includes( :owner, :members, :teams_scorers, :cases, scorers: [ :teams ] ).all + @teams = current_user.teams.includes( :owner, :members, :cases, scorers: [ :teams ] ).all respond_with @teams end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3167f50b3..7d4b575ad 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,10 +20,6 @@ class ApplicationController < ActionController::Base private - def ssl_enabled? - 'true' == ENV['FORCE_SSL'] - end - def signup_enabled? Rails.application.config.signup_enabled end diff --git a/app/controllers/concerns/authentication/current_user_manager.rb b/app/controllers/concerns/authentication/current_user_manager.rb index db1cdd1f8..b17208bd6 100644 --- a/app/controllers/concerns/authentication/current_user_manager.rb +++ b/app/controllers/concerns/authentication/current_user_manager.rb @@ -43,7 +43,7 @@ def set_current_user end def require_login - redirect_to secure_path unless @current_user + redirect_to sessions_path unless @current_user end def auto_login user diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index be17929ce..14609a683 100755 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class HomeController < ApplicationController + # If Quepid is running on HTTPS, like on Heroku, then it needs to switch + # to HTTP in order to make calls to a Solr that is running in HTTP as well, otherwise + # you get this "Mixed Content", which browsers block as a security issue. + # https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content before_action :redirect_to_non_ssl def index diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 854497c2a..547925bf0 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ProfilesController < ApplicationController - force_ssl if: :ssl_enabled? layout 'account' def show; end diff --git a/app/controllers/secure_controller.rb b/app/controllers/secure_controller.rb index ba9b7812d..588fbc127 100644 --- a/app/controllers/secure_controller.rb +++ b/app/controllers/secure_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SecureController < ApplicationController - force_ssl if: :ssl_enabled? skip_before_action :require_login def index diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3d49abd89..79528efee 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,17 +1,32 @@ # frozen_string_literal: true class SessionsController < ApplicationController - force_ssl if: :ssl_enabled? - skip_before_action :require_login, only: :create + skip_before_action :require_login, only: [ :create, :index ] skip_before_action :check_current_user_locked!, only: :create skip_before_action :verify_authenticity_token, only: :create + layout 'start' + + def index + @user = User.new + end + def create - user = login(params[:email], params[:password]) + login_params = user_params + + @user = login(login_params[:email], login_params[:password]) respond_to do |format| - if user + if @user + format.html { redirect_to root_path } format.json { render json: { message: 'connected' }, status: :ok } else + @user = User.new(email: login_params[:email]) + + # rubocop:disable Layout/LineLength + @user.errors.add(:base, + 'Unknown email/password combo. Double check you have the correct email address and password, or sign up for a new account.' ) + # rubocop:enable Layout/LineLength + format.html { render :index } format.json { render json: { reason: @error }, status: :unprocessable_entity } end end @@ -20,7 +35,7 @@ def create def destroy clear_user_session - redirect_to secure_path + redirect_to sessions_path end private @@ -51,4 +66,8 @@ def login email, password user end + + def user_params + params.require(:user).permit(:email, :password) + end end diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb index 3b8ca8f42..e76830b42 100644 --- a/app/controllers/users/invitations_controller.rb +++ b/app/controllers/users/invitations_controller.rb @@ -2,10 +2,9 @@ module Users class InvitationsController < Devise::InvitationsController - force_ssl if: :ssl_enabled? skip_before_action :require_login, only: [ :edit, :update ] - layout 'secure' + layout 'start' # Intercepts the login path and redirects the user to their # Team page as their first page after joining Quepid! @@ -16,7 +15,7 @@ def after_accept_path_for resource def update unless signup_enabled? flash.now[:error] = 'Signups are disabled.' - redirect_to secure_path and return + redirect_to sessions_path and return end super diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..67eb3ad29 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Users + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :require_login, only: [ :keycloakopenid, :google_oauth2, :failure ] + + # rubocop:disable Metrics/AbcSize + def keycloakopenid + Rails.logger.debug(request.env['omniauth.auth']) + @user = create_user_from_omniauth(request.env['omniauth.auth']) + @user.errors.add(:base, "Can't log in a locked user." ) if @user.locked + if @user.persisted? & !@user.locked + session[:current_user_id] = @user.id # this populates our session variable. + + # in this flow, we have a new user joining, so we create a empty case for them, which + # on the home_controller.rb triggers the bootstrap and the new case wizard. + @user.cases.build case_name: "Case #{@user.cases.size}" + + # sign_in_and_redirect @user, event: :authentication + redirect_to root_path + else + Rails.logger.warn('user not persisted, what do we need to do?') + session['devise.keycloakopenid_data'] = request.env['omniauth.auth'] + redirect_to new_user_registration_url + end + end + + def google_oauth2 + Rails.logger.debug(request.env['omniauth.auth']) + @user = create_user_from_omniauth(request.env['omniauth.auth']) + @user.errors.add(:base, "Can't log in a locked user." ) if @user.locked + if @user.errors.empty? + session[:current_user_id] = @user.id # this populates our session variable. + + # in this flow, we have a new user joining, so we create a empty case for them, which + # on the home_controller.rb triggers the bootstrap and the new case wizard. + @user.cases.build case_name: "Case #{@user.cases.size}" + + redirect_to root_path + # sign_in_and_redirect @user, event: :authentication + else + # Removing extra as it can overflow some session stores + session['devise.google_data'] = request.env['omniauth.auth'].except('extra') + redirect_to root_path, alert: @user.errors.full_messages.join("\n") if @user + end + end + # rubocop:enable Metrics/AbcSize + + def failure + redirect_to root_path, alert: 'Could not sign user in with OAuth provider.' + end + + private + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def create_user_from_omniauth auth + if Rails.application.config.signup_enabled + user = User.find_or_initialize_by(email: auth['info']['email']) + else + user = User.find_by(email: auth['info']['email']) + if user.nil? # we looked for a existing user account and didn't find it + user = User.new(email: auth['info']['email']) + user.errors.add(:base, 'You can only sign in with already created users.' ) + end + end + + user.name = auth['info']['name'] + user.password = 'fake' if user.password.blank? # If you don't have a password, fake it. + user.agreed = true + + user.num_logins ||= 0 + user.num_logins += 1 + + user.profile_pic = auth['info']['image'] + # user.access_token = auth['credentials']['token'] + # user.refresh_token = auth['credentials']['refresh_token'] unless auth['credentials']['refresh_token'].nil? + # user.expires_at = auth['credentials']['expires_at'] unless auth['credentials']['refresh_token'].nil? + user.save! if user.errors.empty? + user + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index fbddfff95..22cd1d763 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -4,13 +4,12 @@ module Users class PasswordsController < Devise::PasswordsController include NotificationsManager - force_ssl if: :ssl_enabled? skip_before_action :require_login skip_before_action :require_no_authentication before_action :check_email - layout 'secure' + layout 'start' # GET /resource/password/new # def new diff --git a/app/controllers/users/signups_controller.rb b/app/controllers/users/signups_controller.rb new file mode 100644 index 000000000..4944933a1 --- /dev/null +++ b/app/controllers/users/signups_controller.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Users + class SignupsController < ApplicationController + skip_before_action :require_login + layout 'start' + + # rubocop:disable Metrics/MethodLength + def create + user_params_to_save = user_params + + # Check if we already have an invite out for this user, and if so let's use that + if user_params_to_save[:email].blank? + @user = User.new user_params_to_save + else + @user = User.where(email: user_params_to_save[:email]).where.not(invitation_token: nil).first + if @user + @user.assign_attributes(user_params_to_save) + else + @user = User.new user_params_to_save + # in this flow, we have a new user joining, so we create a empty case for them, which + # on the home_controller.rb triggers the bootstrap and the new case wizard. + @user.cases.build case_name: "Case #{@user.cases.size}" + end + end + + respond_to do |format| + format.html do + if @user.save + session[:current_user_id] = @user.id # not sure if we need to do more here? + Analytics::Tracker.track_signup_event @user + redirect_to root_path + else + render template: 'sessions/index' + end + end + format.js + end + # respond_to do |format| + # if @user.save + # session[:current_user_id] = @user.id # not sure if we need to do more here? + # Analytics::Tracker.track_signup_event @user + # format.html { redirect_to root_path } + # else + # format.html { render template: 'sessions/index' } + # end + # end + end + # rubocop:enable Metrics/MethodLength + + private + + def user_params + params.require(:user).permit( + :name, + :email, + :password, + :password_confirmation, + :agreed, + :email_marketing + ) + end + end +end diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index f03c7c4f2..d7fd97ec6 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -20,7 +20,7 @@ def devise_error_messages! end def devise_error_messages? - resource.errors.empty? ? false : true + !resource.errors.empty? end def devise_reset_password_error_messages diff --git a/app/javascripts/channels/consumer.js b/app/javascripts/channels/consumer.js new file mode 100644 index 000000000..8ec3aad3a --- /dev/null +++ b/app/javascripts/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. + +import { createConsumer } from "@rails/actioncable" + +export default createConsumer() diff --git a/app/javascripts/channels/index.js b/app/javascripts/channels/index.js new file mode 100644 index 000000000..0cfcf7491 --- /dev/null +++ b/app/javascripts/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/app/javascripts/packs/application.js b/app/javascripts/packs/application.js new file mode 100644 index 000000000..f710851a8 --- /dev/null +++ b/app/javascripts/packs/application.js @@ -0,0 +1,13 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import Rails from "@rails/ujs" +import Turbolinks from "turbolinks" +import * as ActiveStorage from "@rails/activestorage" +import "channels" + +Rails.start() +Turbolinks.start() +ActiveStorage.start() diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d92ffddcb..bef395997 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,4 +1,9 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError end diff --git a/app/jobs/google_analytics_event_job.rb b/app/jobs/google_analytics_event_job.rb index ff70afed3..a261de255 100644 --- a/app/jobs/google_analytics_event_job.rb +++ b/app/jobs/google_analytics_event_job.rb @@ -4,9 +4,9 @@ class GoogleAnalyticsEventJob < ApplicationJob queue_as :default def perform data - return unless Analytics::GA.enabled? + return unless Analytics::GoogleAnalytics.enabled? - Analytics::GA.ga.event( + Analytics::GoogleAnalytics.ga.event( data[:category], data[:action], data[:label], diff --git a/app/models/case.rb b/app/models/case.rb index 5fac5dd1c..d09e6b6ca 100644 --- a/app/models/case.rb +++ b/app/models/case.rb @@ -9,7 +9,7 @@ # search_url :string(500) # field_spec :string(500) # last_try_number :integer -# user_id :integer +# owner_id :integer # archived :boolean # scorer_id :integer # created_at :datetime not null @@ -27,7 +27,8 @@ class Case < ApplicationRecord belongs_to :scorer, optional: true - belongs_to :user, optional: true + belongs_to :owner, + class_name: 'User', optional: true has_many :tries, -> { order(try_number: :desc) }, dependent: :destroy, @@ -36,7 +37,7 @@ class Case < ApplicationRecord has_many :metadata, dependent: :destroy - # has_many :ratings, # wed ont' actually need htis. + # has_many :ratings, # we don't actually need this. # through: :queries # rubocop:disable Rails/InverseOf @@ -84,7 +85,7 @@ class Case < ApplicationRecord scope :for_user_directly_owned, ->(user) { where(' - `cases`.`user_id` = ? + `cases`.`owner_id` = ? ', user.id) } @@ -106,7 +107,7 @@ def really_destroy # rubocop:disable Metrics/ParameterLists def clone_case original_case, user, try: nil, clone_queries: false, clone_ratings: false, preserve_history: false transaction do - self.user = user + self.owner = user if preserve_history original_case.tries.each do |a_try| @@ -153,8 +154,8 @@ def last_score def set_scorer return if scorer_id.present? - self.scorer = if user&.default_scorer - user.default_scorer + self.scorer = if owner&.default_scorer + owner.default_scorer else Scorer.system_default_scorer end diff --git a/app/models/concerns/profile.rb b/app/models/concerns/profile.rb index 5e77378c9..0f23cc85f 100644 --- a/app/models/concerns/profile.rb +++ b/app/models/concerns/profile.rb @@ -12,9 +12,13 @@ module Profile }.freeze def avatar_url size = :small - gravatar_id = Digest::MD5.hexdigest(email.downcase) - gravatar_size = size_to_number size - "https://secure.gravatar.com/avatar/#{gravatar_id}.png?s=#{gravatar_size}&d=retro" + if profile_pic.present? + profile_pic + else + gravatar_id = Digest::MD5.hexdigest(email.downcase) + gravatar_size = size_to_number size + "https://secure.gravatar.com/avatar/#{gravatar_id}.png?s=#{gravatar_size}&d=retro" + end end def display_name diff --git a/app/models/try.rb b/app/models/try.rb index 5efd1956e..e9d3d919a 100644 --- a/app/models/try.rb +++ b/app/models/try.rb @@ -81,7 +81,7 @@ def add_curator_vars vars = {} end def curator_vars_map - Hash[curator_variables.map { |each| [ each.name.to_sym, each.value ] }] + curator_variables.map { |each| [ each.name.to_sym, each.value ] }.to_h end def solr_args diff --git a/app/models/user.rb b/app/models/user.rb index 09aa2b289..ddde88683 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -30,8 +30,13 @@ class User < ApplicationRecord # Associations belongs_to :default_scorer, class_name: 'Scorer', optional: true # for communal scorers there isn't a owner + # has_many :cases, + # dependent: :nullify # sometimes a case belongs to a team, so don't just delete it. has_many :cases, - dependent: :nullify # sometimes a case belongs to a team, so don't just delete it. + class_name: 'Case', + foreign_key: :owner_id, + inverse_of: :owner, + dependent: :nullify has_many :queries, through: :cases @@ -91,7 +96,7 @@ class User < ApplicationRecord validates_with ::DefaultScorerExistsValidator validates :agreed, - acceptance: { message: 'You must agree to the terms and conditions.' }, + acceptance: { message: 'checkbox must be clicked to signify you agree to the terms and conditions.' }, if: :terms_and_conditions? def terms_and_conditions? @@ -128,7 +133,9 @@ def check_scorer_ownership_before_removing! # :confirmable, :lockable, :timeoutable and :omniauthable # devise :invitable, :database_authenticatable, :registerable, # :recoverable, :rememberable, :trackable, :validatable - devise :invitable, :recoverable, reset_password_keys: [ :email ] + devise :invitable, :recoverable, :omniauthable, omniauth_providers: [ :keycloakopenid, :google_oauth2 ] + # devise :omniauthable, omniauth_providers: %i[keycloakopenid] + # devise :invitable, :recoverable, :omniauthable # Devise hacks since we only use the recoverable module attr_accessor :password_confirmation diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb index f7f67a614..5c1d7622e 100644 --- a/app/views/admin/users/_form.html.erb +++ b/app/views/admin/users/_form.html.erb @@ -1,4 +1,7 @@ - ++ <%= form_for(@user, url: admin_user_path(@user), class: 'form-horizontal') do |f| %> <% if @user.errors.any? %>+ +@@ -31,9 +34,44 @@-<% end %> +- <%= f.submit class: 'btn btn-primary' %> -+ <%= f.submit class: 'btn btn-primary btn-lg' %>+ +<%= form_for(@user, url: admin_user_path(@user), class: 'form-horizontal') do |f| %> + + + <% if @user.errors.any? %> +diff --git a/app/views/admin/users/_member.html.erb b/app/views/admin/users/_member.html.erb deleted file mode 100644 index d586de2d0..000000000 --- a/app/views/admin/users/_member.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -++ <% end %> + +<%= pluralize(@user.errors.count, "error") %> prohibited this from being saved:
+ ++ <% @user.errors.full_messages.each do |message| %> +
+- <%= message %>
+ <% end %> ++ <%= f.label :password, class: 'control-label' %> + <%= f.password_field :password, class: 'form-control input-lg', placeholder: 'New Password' %> + ++ ++ <%= f.label :password_confirmation, class: 'control-label' %> + <%= f.password_field :password_confirmation, class: 'form-control input-lg', placeholder: 'Confirm New Password' %> ++ ++ ++ <% end %> +
<%= notice %>
+ <%= image_tag @user.avatar_url(:big), size:'96x96', class:'img-rounded' %>