From 1708df3f6c385be6c9a4ddd207069ca1285ceb4e Mon Sep 17 00:00:00 2001 From: benni Date: Mon, 8 Oct 2012 11:51:56 +0200 Subject: [PATCH] Fixed login, new_password and invitation views. --- app/controllers/invites_controller.rb | 2 + app/controllers/login_controller.rb | 16 ++-- app/helpers/application_helper.rb | 13 ++++ app/mailers/mailer.rb | 26 ++++--- app/models/invite.rb | 8 +- app/views/layouts/_header.html.haml | 29 +++++++ app/views/layouts/application.html.haml | 77 +++++++------------ app/views/layouts/login.html.haml | 32 ++++---- ...nvite.haml => accept_invitation.html.haml} | 11 ++- app/views/login/forgot_password.html.haml | 5 +- ...sword.html.haml => new_password.html.haml} | 3 +- app/views/mailer/reset_password.text.erb | 4 +- app/views/sessions/new.html.haml | 28 +++---- app/views/shared/_user_form_fields.html.haml | 15 ++-- config/initializers/delayed_job_config.rb | 1 + config/initializers/simple_form.rb | 4 +- config/initializers/simple_form_bootstrap.rb | 45 +++++++++++ config/routes.rb | 3 + 18 files changed, 200 insertions(+), 122 deletions(-) create mode 100644 app/views/layouts/_header.html.haml rename app/views/login/{invite.haml => accept_invitation.html.haml} (75%) rename app/views/login/{password.html.haml => new_password.html.haml} (78%) create mode 100644 config/initializers/simple_form_bootstrap.rb diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index d763eb9f..48a395c5 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -10,8 +10,10 @@ class InvitesController < ApplicationController def create @invite = Invite.new(params[:invite]) if @invite.save + Mailer.delay.invite(FoodsoftConfig.scope, @invite.id) redirect_to back_or_default_path, notice: "Benutzerin wurde erfolgreich eingeladen." else + logger.debug "[debug] errors: #{@invite.errors.messages}" render action: :new end end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index eaffffcf..b57a8159 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,7 +1,7 @@ # encoding: utf-8 class LoginController < ApplicationController skip_before_filter :authenticate # no authentication since this is the login page - before_filter :validate_token, :only => [:password, :update_password] + before_filter :validate_token, :only => [:new_password, :update_password] # Display the form to enter an email address requesting a token to set a new password. def forgot_password @@ -14,7 +14,7 @@ class LoginController < ApplicationController user.reset_password_token = user.new_random_password(16) user.reset_password_expires = Time.now.advance(:days => 2) if user.save - email = Mailer.reset_password(user).deliver + Mailer.delay.reset_password(FoodsoftConfig.scope, user.id) logger.debug("Sent password reset email to #{user.email}.") end end @@ -23,7 +23,7 @@ class LoginController < ApplicationController # Set a new password with a token from the password reminder email. # Called with params :id => User.id and :token => User.reset_password_token to specify a new password. - def password + def new_password end # Sets a new password. @@ -36,13 +36,13 @@ class LoginController < ApplicationController @user.save redirect_to login_url, :notice => "Dein Passwort wurde aktualisiert. Du kannst Dich jetzt anmelden." else - render :action => 'password' + render :new_password end end - # Invited users. - def invite - @invite = Invite.find_by_token(params[:id]) + # For invited users. + def accept_invitation + @invite = Invite.find_by_token(params[:token]) if (@invite.nil? || @invite.expires_at < Time.now) flash[:error] = "Deine Einladung ist nicht (mehr) gültig." render :action => 'login' @@ -71,7 +71,7 @@ class LoginController < ApplicationController def validate_token @user = User.find_by_id_and_reset_password_token(params[:id], params[:token]) if (@user.nil? || @user.reset_password_expires < Time.now) - flash[:error] = "Ungültiger oder abgelaufener Token. Bitte versuch es erneut." + flash.now.error = "Ungültiger oder abgelaufener Token. Bitte versuch es erneut." render :action => 'forgot_password' end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ba82cb0f..ad2eebc1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -130,4 +130,17 @@ module ApplicationHelper :target => "_blank" end + def bootstrap_flash + flash_messages = [] + flash.each do |type, message| + type = :success if type == :notice + type = :error if type == :alert + text = content_tag(:div, + content_tag(:button, raw("×"), :class => "close", "data-dismiss" => "alert") + + message, :class => "alert fade in alert-#{type}") + flash_messages << text if message + end + flash_messages.join("\n").html_safe + end + end diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 1bae467b..0f74bcd9 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -19,20 +19,22 @@ class Mailer < ActionMailer::Base # Sends an email with instructions on how to reset the password. # Assumes user.setResetPasswordToken has been successfully called already. - def reset_password(user) - @user = user - @link = url_for(:controller => "login", :action => "password", :id => user.id, :token => user.reset_password_token) + def reset_password(foodcoop, user_id) + set_foodcoop_scope(foodcoop) + @user = User.find(user_id) + @link = new_password_url(id: @user.id, token: @user.reset_password_token) - mail :to => user.email, - :subject => "[#{FoodsoftConfig[:name]}] Neues Passwort für/ New password for #{user.nick}" + mail :to => @user.email, + :subject => "[#{FoodsoftConfig[:name]}] Neues Passwort für/ New password for #{@user.nick}" end # Sends an invite email. - def invite(invite) - @invite = invite - @link = url_for(:controller => "login", :action => "invite", :id => invite.token) + def invite(foodcoop, invite_id) + set_foodcoop_scope(foodcoop) + @invite = Invite.find(invite_id) + @link = accept_invitation_url(token: @invite.token) - mail :to => invite.email, + mail :to => @invite.email, :subject => "Einladung in die Foodcoop #{FoodsoftConfig[:name]} - Invitation to the Foodcoop" end @@ -81,5 +83,11 @@ class Mailer < ActionMailer::Base mail :to => user.email, :subject => "[#{FoodsoftConfig[:name]}] \"#{task.name}\" braucht noch Leute!" end + + private + + def set_foodcoop_scope(foodcoop) + ActionMailer::Base.default_url_options[:foodcoop] = foodcoop + end end diff --git a/app/models/invite.rb b/app/models/invite.rb index b90dc3bb..3c6e4d8f 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -12,20 +12,16 @@ class Invite < ActiveRecord::Base validates_presence_of :expires_at validate :email_not_already_registered, :on => :create + before_validation :set_token_and_expires_at protected # Before validation, set token and expires_at. - def before_validation + def set_token_and_expires_at self.token = Digest::SHA1.hexdigest(Time.now.to_s + rand(100).to_s) self.expires_at = Time.now.advance(:days => 7) end - # Sends an email to the invited user. - def after_create - Mailer.invite(self).deliver - end - private # Custom validation: check that email does not already belong to a registered user. diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml new file mode 100644 index 00000000..63c29a3b --- /dev/null +++ b/app/views/layouts/_header.html.haml @@ -0,0 +1,29 @@ +!!! 5 +%html(lang="en") + %head + %meta(charset="utf-8") + %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") + %meta(name="viewport" content="width=device-width, initial-scale=1.0") + %title= content_for?(:title) ? yield(:title) : "Foodsoft" + = csrf_meta_tags + / Le HTML5 shim, for IE6-8 support of HTML elements + /[if lt IE 9] + = javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js" + = stylesheet_link_tag "application", :media => "all" + %link(href="images/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144") + %link(href="images/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114") + %link(href="images/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72") + %link(href="images/apple-touch-icon.png" rel="apple-touch-icon-precomposed") + //%link(href="images/favicon.ico" rel="shortcut icon") + + = yield(:head) + + %body + = yield + + / + Javascripts + \================================================== + / Placed at the end of the document so the pages load faster + = javascript_include_tag "application" + = yield(:javascript) \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index c380f2aa..ec423f02 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,58 +1,35 @@ -!!! 5 -%html(lang="en") - %head - %meta(charset="utf-8") - %meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1") - %meta(name="viewport" content="width=device-width, initial-scale=1.0") - %title= content_for?(:title) ? yield(:title) : "Foodsoft" - = csrf_meta_tags - / Le HTML5 shim, for IE6-8 support of HTML elements - /[if lt IE 9] - = javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js" - = stylesheet_link_tag "application", :media => "all" - %link(href="images/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144") - %link(href="images/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114") - %link(href="images/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72") - %link(href="images/apple-touch-icon.png" rel="apple-touch-icon-precomposed") - %link(href="images/favicon.ico" rel="shortcut icon") += render layout: 'layouts/header' do + .row-fluid + .navbar.navbar-fixed-top + .navbar-inner + .container-fluid + %a.btn.btn-navbar(data-target=".nav-collapse" data-toggle="collapse") + %span.icon-bar + %span.icon-bar + %span.icon-bar + = link_to 'Foodsoft', root_path(anchor: ''), class: 'brand' + .container.nav-collapse + = render_navigation expand_all: true, renderer: :bootstrap - %body + .container-fluid .row-fluid - - .navbar.navbar-fixed-top - .navbar-inner - .container-fluid - %a.btn.btn-navbar(data-target=".nav-collapse" data-toggle="collapse") - %span.icon-bar - %span.icon-bar - %span.icon-bar - = link_to 'Foodsoft', root_path(anchor: ''), class: 'brand' - .container.nav-collapse - = render_navigation expand_all: true, renderer: :bootstrap - - .container-fluid - .row-fluid - - if content_for?(:sidebar) - .span2 - = yield(:sidebar) - .span10 - - if show_title? - .page-header - %h1= yield(:title) - = yield - - else + - if content_for?(:sidebar) + .span2 + = yield(:sidebar) + .span10 + = bootstrap_flash - if show_title? .page-header %h1= yield(:title) = yield - - %footer - %p - Foodsoft, open source software to manage a non-profit food coop. + - else + = bootstrap_flash + - if show_title? + .page-header + %h1= yield(:title) + = yield - / - Javascripts - \================================================== - / Placed at the end of the document so the pages load faster - = javascript_include_tag "application" + %footer + %p + Foodsoft, open source software to manage a non-profit food coop. diff --git a/app/views/layouts/login.html.haml b/app/views/layouts/login.html.haml index be87dfec..42d801c6 100644 --- a/app/views/layouts/login.html.haml +++ b/app/views/layouts/login.html.haml @@ -1,19 +1,13 @@ -!!! -%html - %head - %meta{"http-equiv" => "content-type", :content => "text/html;charset=UTF-8"} - %title= "FoodSoft - " + (yield(:title) or controller.controller_name) - = stylesheet_link_tag 'application' - = javascript_include_tag 'application' - = csrf_meta_tags - = yield(:head) - %body - #login - - if yield(:title) - %h1= yield(:title) - - flash.each do |name, msg| - = content_tag :div, msg, :class => "flash #{name}" - = yield - #meta - Foodcoop - = link_to_if FoodsoftConfig[:homepage], FoodsoftConfig[:name], FoodsoftConfig[:homepage] += render layout: 'layouts/header' do + .container + .row + .span6.offset3 + = bootstrap_flash + - if show_title? + .page-header + %h1= yield(:title) + = yield + + %footer + %p + Foodsoft, open source software to manage a non-profit food coop. \ No newline at end of file diff --git a/app/views/login/invite.haml b/app/views/login/accept_invitation.html.haml similarity index 75% rename from app/views/login/invite.haml rename to app/views/login/accept_invitation.html.haml index 77c37888..ec069ee2 100644 --- a/app/views/login/invite.haml +++ b/app/views/login/accept_invitation.html.haml @@ -1,3 +1,7 @@ +- content_for :javascript do + :javascript + $('user_nick').focus(); + - title "Einladung in die #{FoodsoftConfig[:name]}" %p Du bist eingeladen worden als Mitglied der Gruppe @@ -12,7 +16,8 @@ Gründen, weitergeben. Du kannst auch entscheiden, wieviel deiner persönlichen Daten für alle einsehbar sein sollen. 'Alle' bedeutet hier alle Foodcoop-Mitglieder. Die Administratoren haben aber jederzeit Zugriff auf deine Daten. -= simple_form_for @user, url: {action: 'invite'} do |form| += simple_form_for @user, url: accept_invitation_path do |form| = render partial: 'shared/user_form_fields', locals: {f: form} - = submit_tag "Absenden" -= javascript_tag("$('user_nick').focus()") + .form-actions + = submit_tag "Foodsoft Account erstellen", class: 'btn' + diff --git a/app/views/login/forgot_password.html.haml b/app/views/login/forgot_password.html.haml index 0419169a..cd884ba7 100644 --- a/app/views/login/forgot_password.html.haml +++ b/app/views/login/forgot_password.html.haml @@ -7,5 +7,6 @@ = simple_form_for User.new, url: {action: 'reset_password'} do |form| = form.input :email - = form.submit "Neues Passwort anfordern" - = link_to "Abbrechen", :back \ No newline at end of file + .form-actions + = form.submit "Neues Passwort anfordern", class: 'btn' + = link_to "oder abbrechen", :back \ No newline at end of file diff --git a/app/views/login/password.html.haml b/app/views/login/new_password.html.haml similarity index 78% rename from app/views/login/password.html.haml rename to app/views/login/new_password.html.haml index 3b6b7ceb..d8ddc5f7 100644 --- a/app/views/login/password.html.haml +++ b/app/views/login/new_password.html.haml @@ -3,4 +3,5 @@ = simple_form_for @user, :url => {:action => 'update_password', :id => @user.id, :token => @user.reset_password_token} do |form| = form.input :password = form.input :password_confirmation - = form.submit \ No newline at end of file + .form-actions + = form.submit 'Neues Passwort speichern', class: 'btn' \ No newline at end of file diff --git a/app/views/mailer/reset_password.text.erb b/app/views/mailer/reset_password.text.erb index d49b528f..562166c1 100644 --- a/app/views/mailer/reset_password.text.erb +++ b/app/views/mailer/reset_password.text.erb @@ -1,7 +1,7 @@ Hallo <%= @user.nick %>, du (oder jemand anderes) hat auf der FoodSoft-Website ein neues Passwort angefordert. -Um ein neues Passwort einzugeben, gehe zu: <%= @link %> +Um ein neues Passwort einzugeben, gehe zu: <%= raw @link %> Dieser Link kann nur einmal aufgerufen werden und läuft am <%= I18n.l @user.reset_password_expires %> ab. Wenn du das Passwort nicht ändern möchtest oder diese Email nicht ausgelöst hast, brauchst du nichts zu tun. Dein bisheriges Passwort wurde nicht geändert. @@ -11,7 +11,7 @@ Grüße sendet die Foodsoft! :) Hi <%= @user.nick %>, you have (or someone else has) requested a new password. -In order to choose a new password follow this link: <%= @link %> +In order to choose a new password follow this link: <%= raw @link %> This link works only once and expires on <%= I18n.l @user.reset_password_expires, locale: :en %>. If you don't want to change your password, just ignore this message. Your password hasn't been changed yet. diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index e933c4fe..13413eaa 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,4 +1,4 @@ -- content_for :head do +- content_for :javascript do :javascript $(function() { $('#nick').focus(); @@ -7,21 +7,23 @@ - title "FoodSoft login" %noscript - #javascript-warn.error(style="font-size:1.5em") + .alert.alert-error Achtung, Cookies und Javascript müssen aktiviert sein! = link_to "NoScript", "http://noscript.net/" bitte abschalten. -#login-form.edit_form(style="width:25em") - = form_tag sessions_path do - %p - %label{:for => 'user'} Benutzerin - %br/ += form_tag sessions_path, class: 'form-horizontal' do + .control-group + %label(for='nick' class='control-label') Benutzerin + .controls = text_field_tag 'nick' - %p - %label{:for => 'password'} Passwort - %br/ + + .control-group + %label(for='password' class='control-label') Passwort + .controls = password_field_tag 'password' - = submit_tag "Anmelden" - | - = link_to "Passwort vergessen?", :controller => 'login', :action => 'forgot_password' \ No newline at end of file + + .control-group + .controls + = submit_tag "Anmelden", class: 'btn' + = link_to "Passwort vergessen?", :controller => 'login', :action => 'forgot_password' \ No newline at end of file diff --git a/app/views/shared/_user_form_fields.html.haml b/app/views/shared/_user_form_fields.html.haml index 2fb07a75..ac1f9a5d 100644 --- a/app/views/shared/_user_form_fields.html.haml +++ b/app/views/shared/_user_form_fields.html.haml @@ -3,11 +3,12 @@ = f.input :last_name = f.input :email = f.input :phone -= f.input :password, :required => @user.new_record? += f.input :password, :required => f.object.new_record? = f.input :password_confirmation -- for setting in User::setting_keys.keys - .input.boolean - = check_box_tag "user[setting_attributes][#{setting}]", - '1', @user.settings[setting] == '1' || @user.settings_default(setting), - :class => 'boolean' - %label.boolean{:for => "user[setting_attributes][#{setting}]"}= h User::setting_keys[setting] \ No newline at end of file +.control-group + .controls + - for setting in User::setting_keys.keys + %label.checkbox{:for => "user[setting_attributes][#{setting}]"} + = check_box_tag "user[setting_attributes][#{setting}]", '1', + f.object.settings[setting] == '1' || f.object.settings_default(setting) + = User::setting_keys[setting] diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb index 9e8b25ab..8ac99c5f 100644 --- a/config/initializers/delayed_job_config.rb +++ b/config/initializers/delayed_job_config.rb @@ -1,2 +1,3 @@ Delayed::Worker.max_attempts = 5 Delayed::Worker.delay_jobs = !Rails.env.test? +Delayed::Worker.destroy_failed_jobs = false diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index e3f8d096..b4cb19de 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -98,7 +98,7 @@ SimpleForm.setup do |config| config.label_class = 'control-label' # You can define the class to use on all forms. Default is simple_form. - # config.form_class = :simple_form + config.form_class = 'form-horizontal' # You can define which elements should obtain additional classes # config.generate_additional_classes_for = [:wrapper, :label, :input] @@ -108,7 +108,7 @@ SimpleForm.setup do |config| # Tell browsers whether to use default HTML5 validations (novalidate option). # Default is enabled. - config.browser_validations = false + config.browser_validations = true # Collection of methods to detect if a file type was given. # config.file_methods = [ :mounted_as, :file?, :public_filename ] diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 00000000..1a229676 --- /dev/null +++ b/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,45 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |ba| + ba.use :input + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } + end + end + + config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend| + prepend.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-append' do |append| + append.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. + # Check the Bootstrap docs (http://twitter.github.com/bootstrap) + # to learn about the different styles for forms and inputs, + # buttons and other elements. + config.default_wrapper = :bootstrap +end diff --git a/config/routes.rb b/config/routes.rb index b483ac61..c412eec6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,6 +17,8 @@ Foodsoft::Application.routes.draw do match '/login' => 'sessions#new', :as => 'login' match '/logout' => 'sessions#destroy', :as => 'logout' + get '/login/new_password' => 'login#new_password', as: :new_password + match '/login/accept_invitation/:token' => 'login#accept_invitation', as: :accept_invitation resources :sessions, :only => [:new, :create, :destroy] ########### User specific @@ -174,6 +176,7 @@ Foodsoft::Application.routes.draw do resources :users, :only => [:index] + # TODO: This is very error prone. Better deactivate this catch all route match ':controller(/:action(/:id))(.:format)' end # End of /:foodcoop scope