diff options
author | azul <azul@riseup.net> | 2013-07-06 07:51:54 -0700 |
---|---|---|
committer | azul <azul@riseup.net> | 2013-07-06 07:51:54 -0700 |
commit | a18efa42ddc1cf8692d55f76ca3e92792913f40d (patch) | |
tree | 00527737a38bdafcd2e175bb6caf5e30b3360de1 | |
parent | d03e82b4df5075f796f56fb9568992b0ba0d7c07 (diff) | |
parent | dc98ad8c6445182d60b3f1909e0260ace6fbfca5 (diff) |
Merge pull request #55 from elijh/feature/new-ui
Feature/new ui
106 files changed, 1529 insertions, 760 deletions
@@ -1,5 +1,11 @@ # Development # +## Hacking ## + +Some tips on modifying the views: + +* Many of the forms use [simple_form gem](https://github.com/plataformatec/simple_form) + ## Engines ## Leap Web consists of different Engines. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory. @@ -37,7 +43,7 @@ require File.expand_path('../../lib/leap_web/version.rb', __FILE__) # ... Gem::Specification.new do |s| # ... - s.add_dependency "rails" + s.add_dependency "rails" s.add_dependency "leap_web_core", LeapWeb::Version end ``` diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 049a392..cd90934 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,11 +14,10 @@ //= require jquery_ujs //= require srp //= require bootstrap -//= require bootstrap-editable -//= require bootstrap-editable-rails //= require rails.validations //= require rails.validations.simple_form +//= require leap //= require tickets //= require users diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 25e854e..28206b1 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,29 +1,24 @@ // // import custom scss, content to be set in deployment. // -@import "tail"; +@import "head"; // First import journal variables -@import "bootswatch/cerulean/variables"; +// @import "bootswatch/cerulean/variables"; // // import bootstrap. // @import "bootstrap"; -body { padding-top: 60px; } - @import "bootstrap-responsive"; -table.table-hover .btn { - opacity: 0; -} -table.table-hover tr:hover .btn { - opacity: 1; -} -@import "bootstrap-editable"; +// +// LEAP web app specific overrides +// +@import "leap"; // And finally bootswatch style itself -@import "bootswatch/cerulean/bootswatch"; +// @import "bootswatch/cerulean/bootswatch"; // // import custom scss, content to be set in deployment. diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss new file mode 100644 index 0000000..6268b3b --- /dev/null +++ b/app/assets/stylesheets/leap.scss @@ -0,0 +1,194 @@ +// +// LAYOUT +// + +// This is a trick to be able to use bootstrap fluid layout and also have a max-width. +// It is like having your cake and eating it too. +#main { + *zoom: 1; + margin-left: auto; + margin-right: auto; + width: 1000px; + max-width: 100%; +} + +// +// UTILITY +// + +//.debug { +// outline: 1px solid red; +//} + +.full-width { + width: 100%; +} + +.slim { + margin: 0; +} + +.first { + margin-top: 0; + padding-top: 0; +} + +.last { + margin-bottom: 0; + padding-bottom: 0; +} + +.hidden { + display: none; +} + +// +// ICONS +// + +[class^="big-icon-"], +[class*=" big-icon-"] { + display: inline-block; + width: 32px; + height: 32px; + @include ie7-restore-right-whitespace(); + line-height: 32px; + vertical-align: middle; + background-repeat: no-repeat; + margin-top: 1px; +} + +.big-icon-arrow-down { + background-image: url(/img/32/arrow-down.png) +} + +// +// TYPOGRAPHY +// + +input.large { + font-size: $baseFontSize * 1.25; + line-height: $baseLineHeight * 1.5; +} + +.p { + @extend p; +} + +// +// BOOTSTRAP TWEAKS +// + +// +// Sometimes we really want full width controls, but this flies in the face of +// what bootstrap does for control sizes, so we have to step on bootstrap's +// toes a bit to make this work. +// +input, textarea { + &.full-width { + height: inherit; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + } +} + +// like a label, but with no background +.label-clear { + background-color: $white; + text-shadow: none; + color: $black; +} + +// force a black icon, even if bootstrap thinks differently +.icon-black { + background-image: url(/assets/glyphicons-halflings.png) !important; +} + +// override stupid bootstrap behavior of making the active tab appear non-clickable +// and links not being underline +.nav-tabs > li > a:hover, .sidenav > li > a:hover { + text-decoration: underline; +} +.nav-tabs > .active > a:hover { + cursor: pointer; +} + +// +// TICKETS +// + +.ticket { + td.user { + white-space: nowrap; + } + td.comment { + width: 100%; + } +} + +// +// BORING DEFAULT MASTHEAD +// + +#masthead { + background: #eee; + margin-bottom: 10px; + border-bottom: 1px solid #e6e6e6; + .title { + padding: 20px; + font-size: 1.25em; + } + .sitename { + font-weight: bold; + } +} + +.home-buttons { + text-align: center; + .first { + margin: 20px 0; + } + .download { + a.btn { + width: 14em; + } + } + a.btn { + font-weight: bold; + width: 11em; + margin: 10px auto; + display: block; + } +} + +// +// SIDE NAVIGATION +// + +.user_heading { + margin: 1em 0; + font-weight: bold; +} + +.sidenav { + @extend .nav-tabs; + @extend .nav-stacked; + box-shadow: 0 2px 4px rgba(0,0,0,.1); + li.active { + a, a:hover { + background-color: $linkColor; + color: $white; + border-color: darken($linkColor, 0%); + cursor: pointer; + } + } +} + +// +// USERS +// + +.overview li { + padding: 6px 0; +}
\ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 06b245a..62d9df2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,4 +3,16 @@ class ApplicationController < ActionController::Base ActiveSupport.run_load_hooks(:application_controller, self) + protected + + # + # Allows us to pass through bold text to flash messages. See format_flash() for where this is reversed. + # + # TODO: move to core + # + def bold(str) + "[b]#{str}[/b]" + end + helper_method :bold + end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 4e1983a..120541e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,6 +1,9 @@ class HomeController < ApplicationController def index + if logged_in? + redirect_to user_overview_url(current_user) + end debugger if params[:debug] end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be79..1e79990 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,40 @@ module ApplicationHelper + + # + # determine title for the page + # + def html_title + if content_for?(:title) + yield(:title) + elsif @title + [@title, ' - ', APP_CONFIG[:domain]].join + else + APP_CONFIG[:domain] + end + end + + # + # markup for bootstrap icon + # + # http://twitter.github.io/bootstrap/base-css.html#icons + # + def icon(name, color=nil) + if color.nil? + color_class = nil + elsif color == :black + color_class = 'icon-black' + elsif color == :white + color_class = 'icon-white' + end + "<i class=\"icon-#{name} #{color_class}\"></i> ".html_safe + end + + def big_icon(name, color=nil) + "<i class=\"big-icon-#{name}\"></i> ".html_safe + end + + def format_flash(msg) + html_escape(msg).gsub('[b]', '<b>').gsub('[/b]', '</b>').html_safe + end + end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index c02dcad..8c90436 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,11 +1,12 @@ -Try to fetch a -= link_to "cert", cert_path +%h1= t(:welcome, :provider => APP_CONFIG[:domain]) -%p -Create a -= link_to "ticket", new_ticket_path +%p + We provide secure communication services, including encrypted internet, email (coming soon), and chat (coming later). -- if logged_in? - %p - See all - = link_to "tickets", tickets_path += home_page_buttons + +- if Rails.env == 'development' + .row-fluid + %hr + %p + = link_to "fetch a cert", cert_path diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml new file mode 100644 index 0000000..19af627 --- /dev/null +++ b/app/views/layouts/_content.html.haml @@ -0,0 +1,19 @@ +-# +-# Partial for displaying the page content. This is the only place that content should be displayed. +-# + +- if content_for?(:content) + - content = yield(:content) +- else + - content = yield + +- if @show_navigation + .span2 + = render 'layouts/navigation' + .span10 + = render 'layouts/messages' + = content +- else + .span12 + = render 'layouts/messages' + = content diff --git a/help/Readme.md b/app/views/layouts/_footer.html.haml index e69de29..e69de29 100644 --- a/help/Readme.md +++ b/app/views/layouts/_footer.html.haml diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml new file mode 100644 index 0000000..b459545 --- /dev/null +++ b/app/views/layouts/_header.html.haml @@ -0,0 +1,11 @@ +- if admin? + %ul.nav.nav-tabs + %li{:class => ("active" if controller?('users', 'email_settings', 'overviews') || params[:user_id])} + = link_to t(:users), users_path + %li{:class => ("active" if controller?('tickets') && !params[:user_id])} + = link_to t(:tickets), tickets_path + %li + = link_to t(:logout), logout_path, :method => :delete +- if @user && @show_navigation + .user_heading + = @user.email_address diff --git a/app/views/layouts/_masthead.html.haml b/app/views/layouts/_masthead.html.haml new file mode 100644 index 0000000..280f2c2 --- /dev/null +++ b/app/views/layouts/_masthead.html.haml @@ -0,0 +1,4 @@ +.title + %span.sitename + = APP_CONFIG[:domain] + = t(:user_control_panel)
\ No newline at end of file diff --git a/app/views/layouts/_masthead_noauth.html.haml b/app/views/layouts/_masthead_noauth.html.haml new file mode 100644 index 0000000..6bb1943 --- /dev/null +++ b/app/views/layouts/_masthead_noauth.html.haml @@ -0,0 +1,3 @@ +.title + %span.sitename + = APP_CONFIG[:domain]
\ No newline at end of file diff --git a/app/views/layouts/_messages.html.haml b/app/views/layouts/_messages.html.haml index 80e34d4..7ff985f 100644 --- a/app/views/layouts/_messages.html.haml +++ b/app/views/layouts/_messages.html.haml @@ -1,5 +1,6 @@ -- flash.each do |name, msg| - - if msg.is_a?(String) - %div{:class => "alert alert-#{name == :notice ? "success" : "error"}"} - %a.close{"data-dismiss" => "alert"} × - = content_tag :div, msg, :id => "flash_#{name}" +#messages + - flash.each do |name, msg| + - if msg.is_a?(String) + %div{:class => "alert alert-#{name == :notice ? "success" : "error"}"} + %a.close{"data-dismiss" => "alert"} × + = content_tag :div, format_flash(msg), :id => "flash_#{name}" diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml index b75eed7..b42c1fe 100644 --- a/app/views/layouts/_navigation.html.haml +++ b/app/views/layouts/_navigation.html.haml @@ -1,6 +1,6 @@ -= link_to "Leap Web", root_path, :class => 'brand' -%ul.nav - // = render '/tickets/nav' - -%ul.nav.pull-right - = render '/sessions/nav' +%ul.nav.sidenav + = link_to_navigation t(:overview), user_overview_path(@user), :active => controller?(:overviews) + = link_to_navigation t(:account_settings), edit_user_path(@user), :active => controller?(:users) + = link_to_navigation t(:email_settings), edit_user_email_settings_path(@user), :active => controller?(:email_settings) + = link_to_navigation t(:support_tickets), auto_tickets_path(@user), :active => controller?(:tickets) + = link_to_navigation t(:logout), logout_path, :method => :delete diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e6d22f0..d5adca9 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,23 +1,27 @@ +- @show_navigation = true if @show_navigation.nil? && logged_in? !!! %html %head %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"} - %title= content_for?(:title) ? yield(:title) : "Leap Web" + %title= html_title %meta{:content => content_for?(:description) ? yield(:description) : "Leap Web", :name => "description"} = stylesheet_link_tag "application", :media => "all" = javascript_include_tag "application" = csrf_meta_tags = yield(:head) %body - %header.navbar.navbar-fixed-top - %nav.navbar-inner - .container - = render 'layouts/navigation' - #main{:role => "main"} - .container - .content - .row + #masthead + - if logged_in? + = render 'layouts/masthead' + - elsif params[:controller] != 'home' + = render 'layouts/masthead_noauth' + #main + .container-fluid + - if logged_in? + .row-fluid .span12 - = render 'layouts/messages' - = yield - %footer + = render 'layouts/header' + .row-fluid + = render 'layouts/content' + #footer + = render 'layouts/footer' diff --git a/common_dependencies.rb b/common_dependencies.rb index 1650fee..63c3710 100644 --- a/common_dependencies.rb +++ b/common_dependencies.rb @@ -7,5 +7,7 @@ end group :test, :development do gem 'faker' gem 'factory_girl_rails' + gem 'thin' + gem 'quiet_assets' end diff --git a/config/application.rb b/config/application.rb index 957bb0d..5e52c7b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -59,11 +59,18 @@ module LeapWeb # parameters by using an attr_accessible or attr_protected declaration. # config.active_record.whitelist_attributes = true + ## + ## ASSETS + ## + # Enable the asset pipeline config.assets.enabled = true config.assets.initialize_on_precompile = false # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + + # Set to false in order to see asset requests in the log + config.quiet_assets = true end end diff --git a/config/defaults.yml b/config/defaults.yml index f3b92c0..54c5a23 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -16,9 +16,10 @@ cert_options: &cert_options development: <<: *dev_ca <<: *cert_options - admins: [admin, admin2] - domain: develop.me + admins: [blue, admin, admin2] + domain: example.org secret_token: '550df064dbc5052d9e192b324c1c5a1095c85a2195f88bd6f6829c63b74d8dffa4556494a2e8cc44345a1926be8b6cb17aa4b3f3102d826f5679c3fb57bb7100' + pagination_size: 30 test: <<: *dev_ca @@ -26,8 +27,10 @@ test: admins: [admin, admin2] domain: test.me secret_token: '550df064dbc5052d9e192b324c1c5a1095c85a2195f88bd6f6829c63b74d8dffa4556494a2e8cc44345a1926be8b6cb17aa4b3f3102d826f5679c3fb57bb7100' + pagination_size: 30 production: <<: *cert_options admins: [] - domain: deploy.me + domain: example.net + pagination_size: 30
\ No newline at end of file diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb new file mode 100644 index 0000000..574d169 --- /dev/null +++ b/config/initializers/i18n.rb @@ -0,0 +1,2 @@ + +I18n.available_locales = ['en'] diff --git a/config/locales/en.yml b/config/locales/en.yml index fc61c31..63f1c3e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1 @@ -# Sample localization file for English. Add more files in this directory for other locales. -# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. - en: - hello: "Hello world" - no_such_thing: "No such %{thing}." diff --git a/core/app/assets/javascripts/leap.js b/core/app/assets/javascripts/leap.js new file mode 100644 index 0000000..94e602d --- /dev/null +++ b/core/app/assets/javascripts/leap.js @@ -0,0 +1,7 @@ + +// +// add a bootstrap alert to the page via javascript. +// +function alert_message(msg) { + $('#messages').append('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><span>'+msg+'</span></div>'); +} diff --git a/core/app/helpers/core_helper.rb b/core/app/helpers/core_helper.rb new file mode 100644 index 0000000..a496144 --- /dev/null +++ b/core/app/helpers/core_helper.rb @@ -0,0 +1,13 @@ +# +# Misc. helpers needed throughout. +# +module CoreHelper + + # + # insert common buttons (download, login, etc) + # + def home_page_buttons + render 'common/home_page_buttons' + end + +end
\ No newline at end of file diff --git a/core/app/helpers/navigation_helper.rb b/core/app/helpers/navigation_helper.rb new file mode 100644 index 0000000..19cb934 --- /dev/null +++ b/core/app/helpers/navigation_helper.rb @@ -0,0 +1,82 @@ +module NavigationHelper + + # + # used to create a side navigation link. + # + # Signature is the same as link_to, except it accepts an :active value in the html_options + # + def link_to_navigation(*args) + if args.last.is_a? Hash + html_options = args.pop.dup + active_class = html_options.delete(:active) ? 'active' : nil + html_options[:class] = [html_options[:class], active_class].join(' ') + args << html_options + else + active_class = nil + end + content_tag :li, :class => active_class do + link_to(*args) + end + end + + # + # returns true if params[:action] matches one of the args. + # + def action?(*actions) + actions.detect do |action| + if action.is_a? String + action == action_string + elsif action.is_a? Symbol + if action == :none + action_string == nil + else + action == action_symbol + end + end + end + end + + # + # returns true if params[:controller] matches one of the args. + # + # for example: + # controller?(:me, :home) + # controller?('groups/') <-- matches any controller in namespace 'groups' + # + def controller?(*controllers) + controllers.each do |cntr| + if cntr.is_a? String + if cntr.ends_with?('/') + return true if controller_string.starts_with?(cntr.chop) + end + return true if cntr == controller_string + elsif cntr.is_a? Symbol + return true if cntr == controller_symbol + end + end + return false + end + + private + + def controller_string + @controller_string ||= params[:controller].to_s.gsub(/^\//, '') + end + + def controller_symbol + @controller_symbol ||= params[:controller].gsub(/^\//,'').gsub('/','_').to_sym + end + + def action_string + params[:action] + end + + def action_symbol + @action_symbol ||= if params[:action].present? + params[:action].to_sym + else + nil + end + end + +end diff --git a/core/app/views/common/_home_page_buttons.html.haml b/core/app/views/common/_home_page_buttons.html.haml new file mode 100644 index 0000000..82a5cc2 --- /dev/null +++ b/core/app/views/common/_home_page_buttons.html.haml @@ -0,0 +1,19 @@ +- icon_color = :black + +.home-buttons + .row-fluid.first + .span3 + .download.span6 + %span.link= link_to(big_icon('arrow-down', icon_color) + t(:download_client), "https://downloads.leap.se/client", :class => 'btn btn-large') + %span.info= t(:download_client_info, :provider => content_tag(:b,APP_CONFIG[:domain])).html_safe + .span3 + .row-fluid.second + .login.span4 + %span.link= link_to(icon('ok-sign', icon_color) + t(:login), new_session_path, :class => 'btn') + %span.info= t(:login_info) + .signup.span4 + %span.link= link_to(icon('user', icon_color) + t(:signup), new_user_path, :class => 'btn') + %span.info= t(:signup_info) + .help.span4 + %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), "/tickets/new", :class => 'btn') + %span.info= t(:help_info) diff --git a/core/app/views/kaminari/_first_page.html.haml b/core/app/views/kaminari/_first_page.html.haml new file mode 100644 index 0000000..34436e3 --- /dev/null +++ b/core/app/views/kaminari/_first_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "First" page +-# available local variables +-# url: url to the first page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/core/app/views/kaminari/_gap.html.haml b/core/app/views/kaminari/_gap.html.haml new file mode 100644 index 0000000..51de678 --- /dev/null +++ b/core/app/views/kaminari/_gap.html.haml @@ -0,0 +1,8 @@ +-# Non-link tag that stands for skipped pages... +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li.disabled + = raw(t 'views.pagination.truncate') diff --git a/core/app/views/kaminari/_last_page.html.haml b/core/app/views/kaminari/_last_page.html.haml new file mode 100644 index 0000000..c90433c --- /dev/null +++ b/core/app/views/kaminari/_last_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Last" page +-# available local variables +-# url: url to the last page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/core/app/views/kaminari/_next_page.html.haml b/core/app/views/kaminari/_next_page.html.haml new file mode 100644 index 0000000..ea6cab2 --- /dev/null +++ b/core/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.last? + %li.disabled + %span= raw(t 'views.pagination.next') +- else + %li= link_to(raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote) diff --git a/core/app/views/kaminari/_page.html.haml b/core/app/views/kaminari/_page.html.haml new file mode 100644 index 0000000..2f2f142 --- /dev/null +++ b/core/app/views/kaminari/_page.html.haml @@ -0,0 +1,14 @@ +-# Link showing page number +-# available local variables +-# page: a page object for "this" page +-# url: url to this page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote + +- if page.current? + %li.active + %span= page +- else + %li= link_to(page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}) diff --git a/core/app/views/kaminari/_paginator.html.haml b/core/app/views/kaminari/_paginator.html.haml new file mode 100644 index 0000000..79c5b92 --- /dev/null +++ b/core/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,19 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + .pagination + %ul + -#= first_page_tag unless current_page.first? + = prev_page_tag #unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag #unless current_page.last? + -#= last_page_tag unless current_page.last? diff --git a/core/app/views/kaminari/_prev_page.html.haml b/core/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 0000000..d274bf4 --- /dev/null +++ b/core/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.first? + %li.disabled + %span= raw(t 'views.pagination.previous') +- else + %li= link_to(raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote) diff --git a/core/config/initializers/simple_form_bootstrap.rb b/core/config/initializers/simple_form_bootstrap.rb index 1a22967..c949f5e 100644 --- a/core/config/initializers/simple_form_bootstrap.rb +++ b/core/config/initializers/simple_form_bootstrap.rb @@ -37,6 +37,18 @@ SimpleForm.setup do |config| end end + # + # when you don't want any bootstrap "control-group" or "controls" wrappers. + # + config.wrappers :none, :tag => 'div', :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.use :input + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } + 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, diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml new file mode 100644 index 0000000..38724ca --- /dev/null +++ b/core/config/locales/en.yml @@ -0,0 +1,29 @@ +en: + no_such_thing: "No such %{thing}." + thing_was_successfully_created: "%{thing} was successfully created." + + overview: "Overview" + user_control_panel: "user control panel" + + created: "Created" + created_by_on: "Created by %{user} on %{time}" + updated: "Updated" + + none: "None" + unknown: "Unknown" + admin: "Admin" + anonymous: "Anonymous" + save: "Save" + add: "Add" + remove: "Remove" + changes_saved: "Changes saved successfully." + are_you_sure: "Are you sure? This change cannot be undone." + + download_client: "Download Bitmask" + download_client_info: "The Bitmask application allows you to use %{provider} services. It is available for Linux, Mac, Windows, and Android." + login_info: "Log in to change your account settings, create support tickets, and manage payments." + signup_info: "Sign up for a new user account via this website (it is better if you use the Bitmask application to sign up, but this website works too)." + welcome: "Welcome to %{provider}." + get_help: "Get Help" + help_info: "Can't login? Create a new support ticket anonymously." + example_email: 'user@domain.org' diff --git a/help/README.md b/help/README.md new file mode 100644 index 0000000..c9573e6 --- /dev/null +++ b/help/README.md @@ -0,0 +1 @@ +Implements a simple, clean, and easy to use help ticketing system.
\ No newline at end of file diff --git a/help/app/assets/javascripts/tickets.js b/help/app/assets/javascripts/tickets.js index 5376a6e..bf7965c 100644 --- a/help/app/assets/javascripts/tickets.js +++ b/help/app/assets/javascripts/tickets.js @@ -1,4 +1,4 @@ -$(document).ready(function () { - $.fn.editable.defaults.mode = 'inline'; - $('#title').editable(); -});
\ No newline at end of file +//$(document).ready(function () { +// $.fn.editable.defaults.mode = 'inline'; +// $('#title').editable(); +//});
\ No newline at end of file diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb index 6562048..094612c 100644 --- a/help/app/controllers/tickets_controller.rb +++ b/help/app/controllers/tickets_controller.rb @@ -1,12 +1,13 @@ class TicketsController < ApplicationController + include AutoTicketsPathHelper respond_to :html, :json #has_scope :open, :type => boolean - before_filter :set_strings - before_filter :authorize, :only => [:index] before_filter :fetch_ticket, :only => [:show, :update, :destroy] # don't now have an edit method + before_filter :fetch_user + before_filter :set_title def new @ticket = Ticket.new @@ -20,95 +21,134 @@ class TicketsController < ApplicationController @ticket.created_by = current_user.id if logged_in? @ticket.email = current_user.email_address if logged_in? and current_user.email_address - flash[:notice] = 'Ticket was successfully created.' if @ticket.save + if @ticket.save + flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket)) + end # cannot set this until ticket has been saved, as @ticket.id will not be set - flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) if !logged_in? and flash[:notice] - respond_with(@ticket) - - end - -=begin - def edit - @ticket.comments.build - # build ticket comments? + if !logged_in? and flash[:notice] + flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id)) + end + respond_with(@ticket, :location => auto_ticket_path(@ticket)) end -=end def show @comment = TicketComment.new if !@ticket - redirect_to tickets_path, :alert => "No such ticket" + redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => t(:ticket)) return end end def update + if params[:commit] == 'close' + @ticket.is_open = false + @ticket.save + redirect_to_tickets + elsif params[:commit] == 'open' + @ticket.is_open = true + @ticket.save + redirect_to auto_ticket_path(@ticket) + else + @ticket.attributes = cleanup_ticket_params(params[:ticket]) - if params[:post] #currently changes to title or is_open status - @ticket.attributes = params[:post] - # TODO: do we want to keep the history of title changes? one possibility was adding a comment that said something like 'user changed the title from a to b' + if params[:commit] == 'reply_and_close' + @ticket.close + end - else - params[:ticket][:comments_attributes] = nil if params[:ticket][:comments_attributes].values.first[:body].blank? #unset comments hash if no new comment was typed - @ticket.attributes = params[:ticket] #this will call comments_attributes= - @ticket.close if params[:commit] == @reply_close_str #this overrides is_open selection - # what if there is an update and no new comment? Confirm that there is a new comment to update posted_by: - @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) if @ticket.comments_changed? #protecting posted_by isn't working, so this should protect it. - end - if @ticket.changed? and @ticket.save - flash[:notice] = 'Ticket was successfully updated.' - if @ticket.is_open || !logged_in? - respond_with @ticket - else #for closed tickets with authenticated users, redirect to index. - redirect_to tickets_path + if @ticket.comments_changed? + @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) end - else - #redirect_to [:show, @ticket] # - flash[:alert] = 'Ticket has not been changed' - redirect_to @ticket - #respond_with(@ticket) # why does this go to edit?? redirect??? - end + if @ticket.changed? + if @ticket.save + flash[:notice] = t(:changes_saved) + redirect_to_tickets + else + respond_with @ticket + end + else + redirect_to auto_ticket_path(@ticket) + end + end end def index - @all_tickets = Ticket.for_user(current_user, params, admin?) #for tests, useful to have as separate variable - @tickets = @all_tickets.page(params[:page]).per(10) + @all_tickets = Ticket.search(search_options(params)) + @tickets = @all_tickets.page(params[:page]).per(APP_CONFIG[:pagination_size]) end def destroy # should we allow non-admins to delete their own tickets? i don't think necessary. @ticket.destroy if admin? - redirect_to tickets_path + redirect_to auto_tickets_path + end + + protected + + def set_title + @title = t(:tickets) end private - def ticket_access? - @ticket and (admin? or !@ticket.created_by or (current_user and current_user.id == @ticket.created_by)) + # + # redirects to ticket index, if appropriate. + # otherwise, just redirects to @ticket + # + def redirect_to_tickets + if logged_in? + if params[:commit] == t(:reply_and_close) + redirect_to auto_tickets_path + else + redirect_to auto_ticket_path(@ticket) + end + else + # if we are not logged in, there is no index to view + redirect_to auto_ticket_path(@ticket) + end end - def set_strings - @post_reply_str = 'Post reply' #t :post_reply - @reply_close_str = 'Reply and close' #t :reply_and_close + # + # unset comments hash if no new comment was typed + # + def cleanup_ticket_params(ticket) + if ticket && ticket[:comments_attributes] + if ticket[:comments_attributes].values.first[:body].blank? + ticket[:comments_attributes] = nil + end + end + return ticket + end + + def ticket_access? + @ticket and (admin? or !@ticket.created_by or (current_user and current_user.id == @ticket.created_by)) end def fetch_ticket @ticket = Ticket.find(params[:id]) if !@ticket and admin? - redirect_to tickets_path, :alert => t(:no_such_thing, :thing => 'ticket') + redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => 'ticket') return end access_denied unless ticket_access? end - # not using now, as we are using comment_attributes= from the Ticket model -=begin - def add_comment - comment = TicketComment.new(params[:comment]) - comment.posted_by = User.current.id if User.current #could be nil - comment.posted_at = Time.now # TODO: it seems strange to have this here, and not in model - @ticket.comments << comment + + def fetch_user + if params[:user_id] + @user = User.find_by_param(params[:user_id]) + end + end + + # + # clean up params for ticket search + # + def search_options(params) + params.merge( + :admin_status => params[:user_id] ? 'mine' : 'all', + :user_id => @user ? @user.id : current_user.id, + :is_admin => admin? + ) end -=end + end diff --git a/help/app/helpers/auto_tickets_path_helper.rb b/help/app/helpers/auto_tickets_path_helper.rb new file mode 100644 index 0000000..bb71260 --- /dev/null +++ b/help/app/helpers/auto_tickets_path_helper.rb @@ -0,0 +1,51 @@ +# +# These "auto" forms of the normal ticket path route helpers allow us to do two things automatically: +# +# (1) include the user in the path if appropriate. +# (2) retain the sort params, if appropriate. +# +# Tickets views with a user_id are limited to that user. For admins, they don't need a user_id for any ticket action. +# +# This is available both to the views and the tickets_controller. +# +module AutoTicketsPathHelper + + protected + + def auto_tickets_path(options={}) + options = ticket_view_options.merge options + if @user + user_tickets_path(@user, options) + else + tickets_path(options) + end + end + + def auto_ticket_path(ticket, options={}) + options = ticket_view_options.merge options + if @user + user_ticket_path(@user, ticket, options) + else + ticket_path(ticket, options) + end + end + + def auto_new_ticket_path(options={}) + options = ticket_view_options.merge options + if @user + new_user_ticket_path(@user, options) + else + new_ticket_path(options) + end + end + + private + + def ticket_view_options + hsh = {} + hsh[:open_status] = params[:open_status] if params[:open_status] && !params[:open_status].empty? + hsh[:sort_order] = params[:sort_order] if params[:sort_order] && !params[:sort_order].empty? + hsh + end + +end
\ No newline at end of file diff --git a/help/app/helpers/tickets_helper.rb b/help/app/helpers/tickets_helper.rb index bd2c069..7af50d6 100644 --- a/help/app/helpers/tickets_helper.rb +++ b/help/app/helpers/tickets_helper.rb @@ -1,27 +1,54 @@ module TicketsHelper + # + # FORM HELPERS + # - def status - params[:open_status] || 'open' + # + # hidden fields that should be added to ever ticket form. + # these are use for proper redirection after successful actions. + # + def hidden_ticket_fields + haml_concat hidden_field_tag('open_status', params[:open_status]) + haml_concat hidden_field_tag('sort_order', params[:sort_order]) + haml_concat hidden_field_tag('user_id', params[:user_id]) + "" end - def admin - # do we not want this set for non-admins? the param will be viewable in the url - params[:admin_status] || 'all' + # + # PARAM HELPERS + # + + def search_status + if action?(:index) + params[:open_status] || 'open' + else + nil + end end - def order + def search_order params[:sort_order] || 'updated_at_desc' end + # + # LINK HELPERS + # + def link_to_status(new_status) - label = new_status + ' issues' - link_to label, :open_status => new_status, :admin_status => admin, :sort_order => order + if new_status == "open" + label = t(:open_tickets) + elsif new_status == "closed" + label = t(:closed_tickets) + elsif new_status == "all" + label = t(:all_tickets) + end + link_to label, auto_tickets_path(:open_status => new_status, :sort_order => search_order) end def link_to_order(order_field) - if order.start_with?(order_field) + if search_order.start_with?(order_field) # link for currently-filtered field. Link to other direction of this field. - if order.end_with? 'asc' + if search_order.end_with? 'asc' direction = 'desc' icon_direction = 'up' else @@ -35,8 +62,14 @@ module TicketsHelper direction = 'desc' end - link_to :sort_order => order_field + '_at_' + direction, :open_status => status, :admin_status => admin do - arrow + order_field + ' at' + if order_field == 'updated' + label = t(:updated) + elsif order_field == 'created' + label = t(:created) + end + + link_to auto_tickets_path(:sort_order => order_field + '_at_' + direction, :open_status => search_status) do + arrow + label end end diff --git a/help/app/models/ticket.rb b/help/app/models/ticket.rb index 738487a..8066d0d 100644 --- a/help/app/models/ticket.rb +++ b/help/app/models/ticket.rb @@ -1,49 +1,30 @@ +# +# TODO: thought i should reverse keys for descending, but that didn't work. +# look into whether that should be tweaked, and whether it works okay with +# pagination (seems to now...) +# +# TODO: better validation of email +# +# TODO: don't hardcode strings 'unknown user' and 'unauthenticated user' +# class Ticket < CouchRest::Model::Base - #include ActiveModel::Validations - use_database "tickets" - #require 'securerandom' -=begin - title - created_at - updated_at - email_address - user - user_verified? - admins (list of admins who have commented on the ticket) - code (secret url) -=end - - #belongs_to :user #from leap_web_users. doesn't necessarily belong to a user though - property :created_by, String, :protected => true #Integer #nil unless user was authenticated for ticket creation, #THIS should not be changed after being set - property :regarding_user, String#Integer # form cannot be submitted if they type in a username w/out corresponding ID. this field can be nil. for authenticated ticket creation by non-admins, should this just automatically be set to be same as created_by? or maybe we don't use this field unless created_by is nil? - #also, both created_by and regarding_user could be nil---say user forgets username, or has general question - property :title, String - property :email, String #verify - - #property :user_verified, TrueClass, :default => false #will be true exactly when user is set - #admins - #property :code, String, :protected => true # only should be set if created_by is nil #instead we will just use couchdb ID - property :is_open, TrueClass, :default => true - property :comments, [TicketComment] + + property :created_by, String, :protected => true # nil for anonymous tickets, should never be changed + property :regarding_user, String # may be nil or valid username + property :title, String + property :email, String + property :is_open, TrueClass, :default => true + property :comments, [TicketComment] timestamps! - #before_validation :set_created_by, :set_code, :set_email, :on => :create before_validation :set_email, :set_regarding_user, :on => :create - - #named_scope :open, :conditions => {:is_open => true} #?? - design do - #TODO--clean this all up - #view :by_is_open - #view :by_created_by - view :by_updated_at view :by_created_at - #view :by_is_open_and_created_by view :by_is_open_and_created_at view :by_is_open_and_updated_at @@ -52,63 +33,31 @@ class Ticket < CouchRest::Model::Base end validates :title, :presence => true - #validates :comments, :presence => true #do we want it like this? - - - # html5 has built-in validation which isn't ideal, as it says 'please enter an email address' for invalid email addresses, which implies an email address is required, and it is not. validates :email, :allow_blank => true, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ - #TODO: - #def set_created_by - # self.created_by = User.current if User.current - #end - - def self.for_user(user, options = {}, is_admin = false) - - # TODO: thought i should reverse keys for descending, but that didn't work. look into whether that should be tweaked, and whether it works okay with pagination (seems to now...) - - options[:user_id] = user.id - options[:is_admin] = is_admin - + def self.search(options = {}) @selection = TicketSelection.new(options) @selection.tickets end - #def self.tickets_by_commenter(user_id)#, options = {}) - # Ticket.includes_post_by_and_updated_at.startkey([user_id, 0]).endkey([user_id, Time.now]) - #end - def is_creator_validated? !!created_by end -=begin - def set_code #let's not use this---can use same show url - # ruby 1.9 provides url-safe option---this is not necessarily url-safe - self.code = SecureRandom.hex(8) if !is_creator_validated? - end -=end - - def set_email self.email = nil if self.email == "" - # in controller set to be current users email if that exists end def set_regarding_user self.regarding_user = nil if self.regarding_user == "" end - #not saving with close and reopen, as we will save in update when they are called. - #TODO: not sure if we should bother with these: def close self.is_open = false - #save end def reopen self.is_open = true - #save end def commenters @@ -118,20 +67,21 @@ class Ticket < CouchRest::Model::Base if user = User.find(comment.posted_by) commenters << user.login if user and !commenters.include?(user.login) else - commenters << 'unknown user' if !commenters.include?('unknown user') #todo don't hardcode string 'unknown user' + commenters << 'unknown user' if !commenters.include?('unknown user') end else - commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user') #todo don't hardcode string 'unauthenticated user' + commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user') end end commenters.join(', ') end + # + # update comments. User should be set by controller. + # def comments_attributes=(attributes) - if attributes # could be empty as we will empty if nothing was typed in - comment = TicketComment.new(attributes.values.first) #TicketComment.new(attributes) - #comment.posted_by = User.current.id if User.current #we want to avoid User.current, and current_user won't work here. instead will set in tickets_controller - # what about: comment.posted_by = self.updated_by (will need to add ticket.updated_by) + if attributes + comment = TicketComment.new(attributes.values.first) comment.posted_at = Time.now comments << comment end @@ -144,11 +94,5 @@ class Ticket < CouchRest::Model::Base def regarding_user_actual_user User.find_by_login(self.regarding_user) end -=begin - def validate - if email_address and not email_address.strip =~ RFC822::EmailAddress - errors.add 'email', 'contains an invalid address' - end - end -=end + end diff --git a/help/app/models/ticket_selection.rb b/help/app/models/ticket_selection.rb index bebe5fc..74d5b78 100644 --- a/help/app/models/ticket_selection.rb +++ b/help/app/models/ticket_selection.rb @@ -1,10 +1,20 @@ class TicketSelection + # + # supported options: + # + # user_id: id of the user (uuid string) + # open_status: open | closed | all + # sort_order: updated_at_desc | updated_at_asc | created_at_desc | created_at_asc + # admin_status: mine | all + # is_admin: true | false + # def initialize(options = {}) - @options = options - @options[:open_status] ||= 'open' - @options[:sort_order] ||= 'updated_at_desc' - + @user_id = options[:user_id].gsub /[^a-z0-9]/, '' + @open_status = allow options[:open_status], 'open', 'closed', 'all' + @sort_order = allow options[:sort_order], 'updated_at_desc', 'updated_at_asc', 'created_at_desc', 'created_at_asc' + @admin_status = allow options[:admin_status], 'mine', 'all' + @is_admin = allow options[:is_admin], false, true end def tickets @@ -13,25 +23,32 @@ class TicketSelection protected + def allow(source, *allowed) + if allowed.include?(source) + source + else + allowed.first + end + end def finder_method method = 'by_' method += 'includes_post_by_and_' if only_mine? - method += 'is_open_and_' if @options[:open_status] != 'all' - method += @options[:sort_order].sub(/_(de|a)sc$/, '') + method += 'is_open_and_' if @open_status != 'all' + method += @sort_order.sub(/_(de|a)sc$/, '') end def startkey startkeys = [] - startkeys << @options[:user_id] if only_mine? - startkeys << (@options[:open_status] == 'open') if @options[:open_status] != 'all' + startkeys << @user_id if only_mine? + startkeys << (@open_status == 'open') if @open_status != 'all' startkeys << 0 - startkeys = startkeys.join if startkeys.length == 1 #want string not array if just one thing in array + startkeys = startkeys.join if startkeys.length == 1 # want string not array if just one thing in array startkeys end def endkey - endtime = Time.now + 2.days #TODO. this obviously isn't ideal + endtime = Time.now + 2.days # TODO. this obviously isn't ideal if self.startkey.is_a?(Array) endkeys = self.startkey endkeys.pop @@ -43,12 +60,12 @@ class TicketSelection def order # we have defined the ascending method to return the view itself: - (@options[:sort_order].end_with? 'desc') ? 'descending' : 'ascending' + (@sort_order.end_with? 'desc') ? 'descending' : 'ascending' end def only_mine? - !@options[:is_admin] or (@options[:admin_status] == 'mine') + !@is_admin || @admin_status == 'mine' end end diff --git a/help/app/views/tickets/_admin-nav.html.haml b/help/app/views/tickets/_admin-nav.html.haml deleted file mode 100644 index 0e45c40..0000000 --- a/help/app/views/tickets/_admin-nav.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul.nav.nav-pills.nav-stacked - %li{:class => ("active" if admin == 'mine')} - = link_to 'tickets i admin', {:admin_status => 'mine', :open_status => status, :sort_order => order} - %li{:class => ("active" if admin == 'all')} - = link_to 'all tickets', {:admin_status => 'all', :open_status => status, :sort_order => order} diff --git a/help/app/views/tickets/_comment.html.haml b/help/app/views/tickets/_comment.html.haml index 501ceec..4252eee 100644 --- a/help/app/views/tickets/_comment.html.haml +++ b/help/app/views/tickets/_comment.html.haml @@ -1,20 +1,20 @@ -- # style is super ugly but just for now - if admin? or !comment.private # only show comment if user is admin or comment is not private %tr - %td - - if comment.posted_by_user - %b - = 'Posted by' + (comment.posted_by_user.is_admin? ? ' admin' : '') + ':' - = comment.posted_by_user.login - - else - %b - Unauthenticated post + %td.user + %div + %strong + - if comment.posted_by_user + = comment.posted_by_user.login + - else + = t(:anonymous) + %div= comment.posted_at.to_s(:short) + - if comment.posted_by_user && comment.posted_by_user.is_admin? + %div + %span.label.label-inverse + = t(:admin) - if comment.private - (Private comment) - .pull-right - %b - Posted at: - = comment.posted_at.to_s(:short) - %br - = comment.body - + %div + %span.label.label-important + = t(:private) + %td.comment + = comment.body
\ No newline at end of file diff --git a/help/app/views/tickets/_edit_form.html.haml b/help/app/views/tickets/_edit_form.html.haml new file mode 100644 index 0000000..5252c2e --- /dev/null +++ b/help/app/views/tickets/_edit_form.html.haml @@ -0,0 +1,48 @@ +:ruby + # created by user link + if @ticket.created_by_user + created_by = link_to(@ticket.created_by_user.login, user_overview_path(@ticket.created_by_user)) + else + created_by = t(:anonymous) + end + + # regarding user link + if admin? + if @ticket.regarding_user_actual_user + regarding_user_link = link_to @ticket.regarding_user_actual_user.login, user_overview_path(@ticket.regarding_user_actual_user) + else + regarding_user_link = "(#{t(:unknown)})" + end + else + regarding_user_link = '' + end + += form_for @ticket do |f| + = hidden_ticket_fields + %p.first + - if @ticket.is_open? + %span.label.label-info= t(:open) + - else + %span.label.label-success= t(:closed) + %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe + %div= t(:subject) + = f.text_field :title, :class => 'large full-width' + .row-fluid + .span4 + %div= t(:status) + = f.select :is_open, [[t(:open), "true"], [t(:closed), "false"]] + .span4 + %div= t(:email) + = f.text_field :email + .span4 + %div + = t(:regarding_account) + = regarding_user_link + = f.text_field :regarding_user + = f.button t(:save), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'save' + - if @ticket.is_open? + = f.button t(:close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'close' + - else + = f.button t(:open), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'open' + - if admin? + = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn' diff --git a/help/app/views/tickets/_new_comment.html.haml b/help/app/views/tickets/_new_comment.html.haml deleted file mode 100644 index 96388ea..0000000 --- a/help/app/views/tickets/_new_comment.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -= f.simple_fields_for :comments, @comment do |c| - = c.input :body, :label => 'Comment', :as => :text, :input_html => {:class => "span9", :rows=>4} - - if admin? - = c.input :private, :as => :boolean, :label => false, :inline_label => true diff --git a/help/app/views/tickets/_new_comment_form.html.haml b/help/app/views/tickets/_new_comment_form.html.haml new file mode 100644 index 0000000..8418e01 --- /dev/null +++ b/help/app/views/tickets/_new_comment_form.html.haml @@ -0,0 +1,13 @@ +-# +-# for posting a new comment to an existing ticket. +-# += simple_form_for @ticket, :html => {:class => 'slim'} do |f| + = hidden_ticket_fields + = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c| + = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5} + - if admin? + = c.input :private, :as => :boolean, :label => false, :inline_label => true + = f.button :button, t(:post_reply), :name => 'commit', :class => 'btn-primary', :type => 'submit', :value => 'post_reply' + - if logged_in? && @ticket.is_open + = f.button :button, t(:reply_and_close), :name => 'commit', :class => 'btn', :type => 'submit', :value => 'reply_and_close' + = link_to t(:cancel), auto_tickets_path, :class => :btn diff --git a/help/app/views/tickets/_order-nav.html.haml b/help/app/views/tickets/_order-nav.html.haml deleted file mode 100644 index 9e8bcee..0000000 --- a/help/app/views/tickets/_order-nav.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul.nav.nav-pills.pull-right - %li{:class=> ("active" if order.start_with? 'created_at' )} - = link_to_order('created') - %li{:class=> ("active" if order.start_with? 'updated_at' )} - = link_to_order('updated') diff --git a/help/app/views/tickets/_status-nav.html.haml b/help/app/views/tickets/_status-nav.html.haml deleted file mode 100644 index 69f4248..0000000 --- a/help/app/views/tickets/_status-nav.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%ul.nav.nav-tabs - %li{:class => ("active" if status == 'open')} - = link_to_status 'open' - %li{:class => ("active" if status == 'closed')} - = link_to_status 'closed' - %li{:class => ("active" if status == 'all')} - = link_to_status 'all' diff --git a/help/app/views/tickets/_table-nav.html.haml b/help/app/views/tickets/_table-nav.html.haml deleted file mode 100644 index 635b59b..0000000 --- a/help/app/views/tickets/_table-nav.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.row - .span6 - = render 'tickets/status-nav' - .span4 - = render 'tickets/order-nav' diff --git a/help/app/views/tickets/_tabs.html.haml b/help/app/views/tickets/_tabs.html.haml new file mode 100644 index 0000000..b7b5d3a --- /dev/null +++ b/help/app/views/tickets/_tabs.html.haml @@ -0,0 +1,23 @@ +-# +-# SORT ORDER TABS +-# +- unless action?(:new) + %ul.nav.nav-pills.pull-right.slim + %li{:class=> ("active" if search_order.start_with? 'created_at')} + = link_to_order('created') + %li{:class=> ("active" if search_order.start_with? 'updated_at')} + = link_to_order('updated') + +-# +-# STATUS FILTER TABS +-# +%ul.nav.nav-tabs + - if logged_in? + %li{:class => ("active" if search_status == 'open')} + = link_to_status 'open' + %li{:class => ("active" if search_status == 'closed')} + = link_to_status 'closed' + %li{:class => ("active" if search_status == 'all')} + = link_to_status 'all' + %li{:class => ("active" if action?(:new))} + = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path diff --git a/help/app/views/tickets/_ticket.html.haml b/help/app/views/tickets/_ticket.html.haml index 7b37652..a064c4e 100644 --- a/help/app/views/tickets/_ticket.html.haml +++ b/help/app/views/tickets/_ticket.html.haml @@ -1,17 +1,6 @@ -- updated_at_text = 'updated: ' + ticket.updated_at.to_s(:long) +- url = auto_ticket_path(ticket) %tr - %td - %b - = link_to ticket.title, ticket - - if params[:controller] == 'tickets' - %br - %small - created: - = ticket.created_at.to_s(:long) - = updated_at_text - %small.pull-right - comments by: - = ticket.commenters - - else - %small - = updated_at_text
\ No newline at end of file + %td= link_to ticket.title, url + %td= link_to ticket.created_at.to_s(:short), url + %td= link_to ticket.updated_at.to_s(:short), url + %td= ticket.commenters diff --git a/help/app/views/tickets/_ticket_data.html.haml b/help/app/views/tickets/_ticket_data.html.haml deleted file mode 100644 index 6a1a896..0000000 --- a/help/app/views/tickets/_ticket_data.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -.spam12 - %b - Created by: - - if @ticket.created_by_user - = link_to @ticket.created_by_user.login, user_path(@ticket.created_by_user) - - else - Unauthenticated ticket creator - - if @ticket.regarding_user - %b - Regarding user: - - if admin? - - if @ticket.regarding_user_actual_user - = link_to @ticket.regarding_user_actual_user.login, user_path(@ticket.regarding_user_actual_user) - - else - = @ticket.regarding_user + ' (no such user)' - - else # a non-admin is viewing the ticket, so they shouldn't see confirmation of whether the regarding_user exists or not. - = @ticket.regarding_user - - if @ticket.email - %b - email: - = @ticket.email - %b - Created at: - = @ticket.created_at.to_s(:short) - %b - Updated at: - = @ticket.updated_at.to_s(:short) - %b - = "Status:" - - if @ticket.is_open - = 'open' - = button_to 'Close', {:post => {:is_open => false}}, :method => :put, :class => 'btn btn-small' - - else - = 'closed' - = button_to 'Open', {:post => {:is_open => true}}, :method => :put, :class => 'btn btn-small' diff --git a/help/app/views/tickets/index.html.haml b/help/app/views/tickets/index.html.haml index 23a503d..c02a326 100644 --- a/help/app/views/tickets/index.html.haml +++ b/help/app/views/tickets/index.html.haml @@ -1,17 +1,19 @@ -%h1 tickets index +- @show_navigation = !params[:user_id].nil? -Create a -= link_to "new ticket", new_ticket_path += render 'tickets/tabs' -= #%div{"data-pjax-container" => ""} # not sure how to get this working right -.row - .span2 - - if admin? - = render 'tickets/admin-nav' - .span10 - = render 'tickets/table-nav' - %table.table-striped.table-bordered.table-hover{:style => "width:100%;"} - %tbody - = render @tickets.all - = paginate @tickets +%table.table.table-striped.table-bordered + %thead + %tr + %th= t(:subject) + %th= t(:created) + %th= t(:updated) + %th= t(:voices) + %tbody + - if @tickets.any? + = render @tickets.all + - else + %tr + %td{:colspan=>4}= t(:none) += paginate @tickets diff --git a/help/app/views/tickets/new.html.haml b/help/app/views/tickets/new.html.haml index 1aa689b..4d35659 100644 --- a/help/app/views/tickets/new.html.haml +++ b/help/app/views/tickets/new.html.haml @@ -1,10 +1,30 @@ -.span12 - %h2=t :new_ticket - = simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| - = f.input :title - = f.input :email if !current_user #hmm--might authenticated users want to submit an alternate email? +- @show_navigation = !params[:user_id].nil? + += render 'tickets/tabs' + +- if admin? && @user + - email = @user.email_address + - regarding = @user.login +- elsif logged_in? + - email = current_user.email_address + - regarding = current_user.login + += simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f| + = hidden_ticket_fields + = f.input :title, :label => t(:subject) + - if logged_in? + = f.input :email, input_html: {value: email} + = f.input :regarding_user, input_html: {value: regarding} + - else + = f.input :email = f.input :regarding_user - = render :partial => 'new_comment', :locals => {:f => f} - .form-actions - = f.button :submit, :class => 'btn-primary' - = link_to t(:cancel), tickets_path, :class => :btn + = f.simple_fields_for :comments, @comment do |c| + = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5} + - if admin? + = c.input :private, :as => :boolean, :label => false, :inline_label => true + .form-actions + = f.button :submit, :class => 'btn-primary' + - if logged_in? + = link_to t(:cancel), auto_tickets_path, :class => :btn + - else + = link_to t(:cancel), root_path, :class => 'btn'
\ No newline at end of file diff --git a/help/app/views/tickets/show.html.haml b/help/app/views/tickets/show.html.haml index a69048b..bfdb773 100644 --- a/help/app/views/tickets/show.html.haml +++ b/help/app/views/tickets/show.html.haml @@ -1,18 +1,12 @@ -.spam12 - %h2 - %a#title.editable.editable-click{"data-name" => "title", "data-resource" => "post", "data-type" => "text", "data-url" => ticket_path(@ticket.id), "data-pk" => @ticket.id, :href => "#"} - = @ticket.title - = render 'tickets/ticket_data' - %table.table-striped.table-bordered.table-hover{:style => "width:100%;"} - %tbody - = render(:partial => "comment", :collection => @ticket.comments) - = #render @ticket.comments should work if view is in /app/views/comments/_comment +- @show_navigation = !params[:user_id].nil? - = simple_form_for @ticket, :html => {:class => 'form-horizontal'} do |f| # don't need validations so long as this is so simple - = render :partial => 'new_comment', :locals => {:f => f} - .span10.offset3 - = f.button :submit, @post_reply_str, :class => 'btn-primary' - - if @ticket.is_open - = f.button :submit, @reply_close_str - = link_to t(:Destroy), ticket_path, :confirm => 'are you sure?', :method => :delete, :class => 'btn btn-danger' if admin? - = link_to t(:cancel), tickets_path, :class => :btn +.ticket + = render 'tickets/edit_form' + %table.table.table-striped.table-bordered + %tbody + = render :partial => 'tickets/comment', :collection => @ticket.comments + %tr + %td.user + = logged_in? ? current_user.login : t(:anonymous) + %td.comment + = render 'tickets/new_comment_form'
\ No newline at end of file diff --git a/help/config/locales/en.yml b/help/config/locales/en.yml index 4ea662a..79e745f 100644 --- a/help/config/locales/en.yml +++ b/help/config/locales/en.yml @@ -1,2 +1,22 @@ en: - access_ticket_text: "You can later access this ticket at the url %{full_url}. You might want to bookmark this page to find it again. Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to remove it from the browser history" + access_ticket_text: > + You can later access this ticket at the url %{full_url}. You might want to bookmark this page to find it again. + Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to + remove it from the browser history. + support_tickets: "Support Tickets" + all_tickets: "All Tickets" + my_tickets: "My Tickets" + open_tickets: "Open Tickets" + closed_tickets: "Closed Tickets" + new_ticket: "New Ticket" + tickets: "Tickets" + subject: "Subject" + destroy: "Destroy" + open: "Open" + closed: "Closed" + close: "Close" + post_reply: "Post Reply" + reply_and_close: "Reply and Close" + description: "Description" + ticket: "Ticket" + regarding_account: "Regarding Account"
\ No newline at end of file diff --git a/help/config/routes.rb b/help/config/routes.rb index 86a9201..8f3241c 100644 --- a/help/config/routes.rb +++ b/help/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do - - resources :tickets, :only => [:new, :create, :index, :show, :update, :destroy] - #resources :ticket, :only => [:show] + resources :tickets, :except => :edit + resources :users do + resources :tickets, :except => :edit + end end diff --git a/help/leap_web_help.gemspec b/help/leap_web_help.gemspec index 09827dc..4914694 100644 --- a/help/leap_web_help.gemspec +++ b/help/leap_web_help.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = "Help Desk for LeapWeb" s.description = "Managing Tickets for a Leap provider" - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"] + s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] s.test_files = Dir["test/**/*"] s.add_dependency "leap_web_core", LeapWeb::VERSION diff --git a/help/test/functional/tickets_controller_test.rb b/help/test/functional/tickets_controller_test.rb index f8627d8..6479ba4 100644 --- a/help/test/functional/tickets_controller_test.rb +++ b/help/test/functional/tickets_controller_test.rb @@ -181,23 +181,24 @@ class TicketsControllerTest < ActionController::TestCase test "admin_status mine vs all" do testticket = FactoryGirl.create :ticket + user = find_record :user login :is_admin? => true, :email => nil - get :index, {:admin_status => "all", :open_status => "open"} + get :index, {:open_status => "open"} assert assigns(:all_tickets).include?(testticket) - get :index, {:admin_status => "mine", :open_status => "open"} + get :index, {:user_id => user.id, :open_status => "open"} assert !assigns(:all_tickets).include?(testticket) testticket.destroy end test "commenting on a ticket adds to tickets that are mine" do testticket = FactoryGirl.create :ticket - login :is_admin? => true, :email => nil - - get :index, {:admin_status => "mine", :open_status => "open"} + user = find_record :admin_user + login user + get :index, {:user_id => user.id, :open_status => "open"} assert_difference('assigns[:all_tickets].count') do put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}} - get :index, {:admin_status => "mine", :open_status => "open"} + get :index, {:user_id => user.id, :open_status => "open"} end assert assigns(:all_tickets).include?(assigns(:ticket)) diff --git a/public/img/32/arrow-down.png b/public/img/32/arrow-down.png Binary files differnew file mode 100644 index 0000000..3b16e6d --- /dev/null +++ b/public/img/32/arrow-down.png diff --git a/ui_dependencies.rb b/ui_dependencies.rb index b24eef1..2cfd851 100644 --- a/ui_dependencies.rb +++ b/ui_dependencies.rb @@ -4,10 +4,16 @@ gem "jquery-rails" gem "simple_form" gem 'client_side_validations' gem 'client_side_validations-simple_form' -gem 'kaminari', "0.13.0" # for pagination. trying 0.13.0 as there seem to be issues with 0.14.0 when using couchrest -gem 'bootstrap-editable-rails', "~>0.0.4" gem "bootswatch-rails", "~> 0.5.0" +gem 'kaminari', "0.13.0" # for pagination. trying 0.13.0 as there seem to be + # issues with 0.14.0 when using couchrest + +gem 'rails-i18n' # locale files for built-in validation messages and times + # https://github.com/svenfuchs/rails-i18n + # for a list of keys: + # https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml + group :assets do gem "haml-rails", "~> 0.3.4" gem "sass-rails", "~> 3.2.5" diff --git a/users/README.rdoc b/users/README.rdoc deleted file mode 100644 index 9fb44c1..0000000 --- a/users/README.rdoc +++ /dev/null @@ -1,3 +0,0 @@ -= LeapWebUsers - -This project rocks and uses MIT-LICENSE.
\ No newline at end of file diff --git a/users/app/assets/javascripts/users.js b/users/app/assets/javascripts/users.js new file mode 100644 index 0000000..1d05692 --- /dev/null +++ b/users/app/assets/javascripts/users.js @@ -0,0 +1,74 @@ +(function() { + // + // LOCAL FUNCTIONS + // + + var poll_users, prevent_default, form_failed, form_passed; + + prevent_default = function(event) { + return event.preventDefault(); + }; + + poll_users = function(query, process) { + return $.get("/1/users.json", { + query: query + }).done(process); + }; + + // + // PUBLIC FUNCTIONS + // + + srp.session = new srp.Session(); + + srp.signedUp = function() { + return srp.login(); + }; + + srp.loggedIn = function() { + return window.location = '/'; + }; + + srp.updated = function() { + return window.location = '/users/' + srp.session.id(); + }; + + // + // if a json request returns an error, this function gets called and + // decorates the appropriate fields with the error messages. + // + srp.error = function(message) { + var element, error, field; + if ($.isPlainObject(message) && message.errors) { + for (field in message.errors) { + error = message.errors[field]; + element = $('form input[name$="[' + field + ']"]'); + if (!element) { + next; + } + element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false); + } + } else if (message.error) { + alert_message(message.error); + } else { + alert_message(JSON.stringify(message)); + } + }; + + // + // INIT + // + + $(document).ready(function() { + $('#new_user').submit(prevent_default); + $('#new_user').submit(srp.signup); + $('#new_session').submit(prevent_default); + $('#new_session').submit(srp.login); + $('#update_login_and_password').submit(prevent_default); + $('#update_login_and_password').submit(srp.update); + return $('#user-typeahead').typeahead({ + source: poll_users + }); + }); + +}).call(this); diff --git a/users/app/assets/javascripts/users.js.coffee b/users/app/assets/javascripts/users.js.coffee deleted file mode 100644 index 955556c..0000000 --- a/users/app/assets/javascripts/users.js.coffee +++ /dev/null @@ -1,46 +0,0 @@ -preventDefault = (event) -> - event.preventDefault() - -srp.session = new srp.Session() -srp.signedUp = -> - srp.login() - -srp.loggedIn = -> - window.location = '/' - -#// TODO: not sure this is what we want. -srp.updated = -> - window.location = '/' - -srp.error = (message) -> - if $.isPlainObject(message) && message.errors - for field, error of message.errors - element = $('form input[name$="['+field+']"]') - next unless element - element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false) - else - alert(message) - -pollUsers = (query, process) -> - $.get( "/users.json", query: query).done(process) - -followLocationHash = -> - location = window.location.hash - if location - href_select = 'a[href="' + location + '"]' - link = $(href_select) - link.tab('show') if link - -$(document).ready -> - followLocationHash() - $('#new_user').submit preventDefault - $('#new_user').submit srp.signup - $('#new_session').submit preventDefault - $('#new_session').submit srp.login - $('.user.form.update_login_and_password').submit srp.update - $('.user.form.update_login_and_password').submit preventDefault - $('.user.typeahead').typeahead({source: pollUsers}); - $('a[data-toggle="tab"]').on('shown', -> - $(ClientSideValidations.selectors.forms).validate() - ) - diff --git a/users/Readme.md b/users/app/controllers/account_settings_controller.rb index e69de29..e69de29 100644 --- a/users/Readme.md +++ b/users/app/controllers/account_settings_controller.rb diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb index f0a6564..72df7a7 100644 --- a/users/app/controllers/controller_extension/authentication.rb +++ b/users/app/controllers/controller_extension/authentication.rb @@ -38,9 +38,18 @@ module ControllerExtension::Authentication end def access_denied - # TODO: should we redirect to the root_url in either case, and have the root_url include the login screen (and also ability to create unauthenticated tickets) when no user is logged in? - redirect_to login_url, :alert => "Not authorized" if !logged_in? - redirect_to root_url, :alert => "Not authorized" if logged_in? + respond_to do |format| + format.html do + if logged_in? + redirect_to root_url, :alert => t(:not_authorized) + else + redirect_to login_url, :alert => t(:not_authorized_login) + end + end + format.json do + render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity + end + end end def admin? diff --git a/users/app/controllers/email_aliases_controller.rb b/users/app/controllers/email_aliases_controller.rb index 3b0d5ac..4628a7f 100644 --- a/users/app/controllers/email_aliases_controller.rb +++ b/users/app/controllers/email_aliases_controller.rb @@ -1,20 +1,12 @@ -class EmailAliasesController < ApplicationController - +class EmailAliasesController < UsersBaseController before_filter :fetch_user - respond_to :html - def destroy @alias = @user.email_aliases.delete(params[:id]) - @user.save - flash[:notice] = t(:email_alias_destroyed_successfully, :alias => @alias) - redirect_to edit_user_path(@user, :anchor => :email) + if @user.save + flash[:notice] = t(:email_alias_destroyed_successfully, :alias => bold(@alias)) + end + redirect_to edit_user_email_settings_path(@user) end - protected - - def fetch_user - @user = User.find_by_param(params[:user_id]) - access_denied unless admin? or (@user == current_user) - end end diff --git a/users/app/controllers/email_settings_controller.rb b/users/app/controllers/email_settings_controller.rb new file mode 100644 index 0000000..f7d85be --- /dev/null +++ b/users/app/controllers/email_settings_controller.rb @@ -0,0 +1,41 @@ +class EmailSettingsController < UsersBaseController + + before_filter :authorize + before_filter :fetch_user + + def edit + @email_alias = LocalEmail.new + end + + def update + @user.attributes = cleanup_params(params[:user]) + if @user.changed? + if @user.save + flash[:notice] = t(:changes_saved) + redirect + else + if @user.email_aliases.last && !@user.email_aliases.last.valid? + # display bad alias in text field: + @email_alias = @user.email_aliases.pop + end + render 'email_settings/edit' + end + else + redirect + end + end + + private + + def redirect + redirect_to edit_user_email_settings_url(@user) + end + + def cleanup_params(user) + if !user['email_forward'].nil? && user['email_forward'].empty? + user.delete('email_forward') # don't allow "" as an email forward + end + user + end + +end diff --git a/users/app/controllers/overviews_controller.rb b/users/app/controllers/overviews_controller.rb new file mode 100644 index 0000000..52ce267 --- /dev/null +++ b/users/app/controllers/overviews_controller.rb @@ -0,0 +1,9 @@ +class OverviewsController < UsersBaseController + + before_filter :authorize + before_filter :fetch_user + + def show + end + +end diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb index 01ecff6..d6c455b 100644 --- a/users/app/controllers/sessions_controller.rb +++ b/users/app/controllers/sessions_controller.rb @@ -22,4 +22,15 @@ class SessionsController < ApplicationController logout redirect_to root_path end + + # + # this is a bad hack, but user_overview_url(user) is not available + # also, this doesn't work because the redirect happens as a PUT. no idea why. + # + #Warden::Manager.after_authentication do |user, auth, opts| + # response = Rack::Response.new + # response.redirect "/users/#{user.id}/overview" + # throw :warden, response.finish + #end + end diff --git a/users/app/controllers/users_base_controller.rb b/users/app/controllers/users_base_controller.rb new file mode 100644 index 0000000..dc2fa16 --- /dev/null +++ b/users/app/controllers/users_base_controller.rb @@ -0,0 +1,18 @@ +# +# common base class for all user related controllers +# + +class UsersBaseController < ApplicationController + + protected + + def fetch_user + @user = User.find_by_param(params[:user_id] || params[:id]) + if !@user && admin? + redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') + elsif !admin? && @user != current_user + access_denied + end + end + +end diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb index 38a69e3..4ce970b 100644 --- a/users/app/controllers/users_controller.rb +++ b/users/app/controllers/users_controller.rb @@ -1,74 +1,42 @@ -class UsersController < ApplicationController +# +# This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb +# - before_filter :authorize, :only => [:show, :edit, :destroy, :update] +class UsersController < UsersBaseController + + before_filter :authorize, :only => [:show, :edit, :update, :destroy] before_filter :fetch_user, :only => [:show, :edit, :update, :destroy] - before_filter :authorize_self, :only => [:update] - before_filter :set_anchor, :only => [:edit, :update] before_filter :authorize_admin, :only => [:index] - respond_to :json, :html + respond_to :html def index if params[:query] - @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + if @user = User.find_by_login(params[:query]) + redirect_to user_overview_url(@user) + return + else + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + end else @users = User.by_created_at.descending end - @users = @users.limit(10) - respond_with @users.map(&:login).sort + @users = @users.limit(100) end def new @user = User.new end - def create - @user = User.create(params[:user]) - respond_with @user + def show end def edit - @email_alias = LocalEmail.new - end - - def update - @user.attributes = params[:user] - if @user.changed? and @user.save - flash[:notice] = t(:user_updated_successfully) - elsif @user.email_aliases.last and !@user.email_aliases.last.valid? - @email_alias = @user.email_aliases.pop - end - respond_with @user, :location => edit_user_path(@user, :anchor => @anchor) end def destroy @user.destroy - redirect_to admin? ? users_path : root_path + redirect_to admin? ? users_url : root_url end - protected - - def fetch_user - # authorize filter has been checked first, so won't get here unless authenticated - @user = User.find_by_param(params[:id]) - if !@user and admin? - redirect_to users_path, :alert => t(:no_such_thing, :thing => 'user') - return - end - access_denied unless admin? or (@user == current_user) - end - - def authorize_self - # have already checked that authorized - access_denied unless (@user == current_user) - end - - def set_anchor - @anchor = email_settings? ? :email : :account - end - - def email_settings? - params[:user] && - params[:user].keys.detect{|key| key.index('email')} - end end diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb index 617bd4b..fda56f2 100644 --- a/users/app/controllers/v1/users_controller.rb +++ b/users/app/controllers/v1/users_controller.rb @@ -1,20 +1,32 @@ module V1 - class UsersController < ApplicationController + class UsersController < UsersBaseController skip_before_filter :verify_authenticity_token + before_filter :fetch_user, :only => [:update] before_filter :authorize, :only => [:update] + before_filter :authorize_admin, :only => [:index] respond_to :json + def index + if params[:query] + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + respond_with @users.map(&:login).sort + else + render :json => {'error' => 'query required', 'status' => :unprocessable_entity} + end + end + def create @user = User.create(params[:user]) respond_with @user # return ID instead? end def update - # For now, only allow public key to be updated via the API. Eventually we might want to store in a config what attributes can be updated via the API. - @user = User.find_by_param(params[:id]) - @user.update_attributes params[:user].slice(:public_key) if params[:user].respond_to?(:slice) + @user.update_attributes params[:user] + if @user.valid? + flash[:notice] = t(:user_updated_successfully) + end respond_with @user end diff --git a/users/app/helpers/users_helper.rb b/users/app/helpers/users_helper.rb index 9feae62..f56faab 100644 --- a/users/app/helpers/users_helper.rb +++ b/users/app/helpers/users_helper.rb @@ -1,39 +1,7 @@ module UsersHelper - def user_form_with(partial, options = {}) - user_form(options) do |f| - options[:f] = f - render :partial => partial, - :layout => 'legend_and_submit', - :locals => options - end - end - - def user_form(options = {}) - simple_form_for @user, - :html => user_form_html_options(options), - :validate => true do |f| - yield f - end - end - - def user_form_html_options(options) - { :class => user_form_html_classes(options).join(" "), - :id => dom_id(@user, options[:legend]) - } - end - - def user_form_html_classes(options) - classes = %W/form-horizontal user form/ - classes << options[:legend] - classes << (@user.new_record? ? 'new' : 'edit') - classes.compact - end - - def user_field(field) - value = @user.send(field) - value = value.to_s(:long) if field.end_with? '_at' - value || 'not set' + def user_form_class(*classes) + (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') end def wrapped(item, options = {}) diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 5c849f0..3459520 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -42,6 +42,7 @@ class User < CouchRest::Model::Base :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } validates :email_forward, + :allow_blank => true, :format => { :with => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/, :message => "needs to be a valid email address"} timestamps! diff --git a/users/app/views/email_settings/edit.html.haml b/users/app/views/email_settings/edit.html.haml new file mode 100644 index 0000000..7757a31 --- /dev/null +++ b/users/app/views/email_settings/edit.html.haml @@ -0,0 +1,38 @@ +- form_options = {:url => user_email_settings_path(@user), :html => {:class => 'form-horizontal'}, :validate => true} +- alias_error_class = @email_alias.username && !@email_alias.valid? ? 'error' : '' + +- content_for :head do + :css + table.aliases tr:first-child td { + border-top: none; + } + += simple_form_for @user, form_options.dup do |f| + %legend= t(:email_aliases) + .control-group + %label.control-label= t(:current_aliases) + .controls + %table.table.table-condensed.no-header.slim.aliases + - if @user.email_aliases.any? + - @user.email_aliases.each do |email| + %tr + %td= email + %td= link_to(icon(:remove) + t(:remove), user_email_alias_path(@user, email), :method => :delete) + - else + %tr + %td{:colspan=>2}= t(:none) + .control-group{:class => alias_error_class} + %label.control-label= t(:add_email_alias) + .controls + = f.simple_fields_for :email_aliases, @email_alias do |e| + .input-append + = e.input_field :username + = e.submit t(:add), :class => 'btn' + = e.error :username + += simple_form_for @user, form_options do |f| + %legend= t(:advanced_options) + = f.input :email_forward + = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4} + .form-actions + = f.submit t(:save), :class => 'btn btn-primary' diff --git a/users/app/views/emails/_email.html.haml b/users/app/views/emails/_email.html.haml index c81b396..ea59cec 100644 --- a/users/app/views/emails/_email.html.haml +++ b/users/app/views/emails/_email.html.haml @@ -3,4 +3,4 @@ - if local_assigns[:with].try(:include?, :delete) = link_to(user_email_alias_path(@user, email), :method => :delete) do %i.icon-remove -.clearfix + diff --git a/users/app/views/overviews/show.html.haml b/users/app/views/overviews/show.html.haml new file mode 100644 index 0000000..b8ad814 --- /dev/null +++ b/users/app/views/overviews/show.html.haml @@ -0,0 +1,18 @@ +.overview + + %h2.first= t(:overview_welcome, :username => @user.login) + + - if admin? + %p + = t(:created) + = @user.created_at + %br + = t(:updated) + = @user.updated_at + + %p= t(:overview_intro) + + %ul.unstyled + %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user)) + %li= icon('envelope') + link_to(t(:overview_email), edit_user_email_settings_path(@user)) + %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user)) diff --git a/users/app/views/sessions/_admin_nav.html.haml b/users/app/views/sessions/_admin_nav.html.haml deleted file mode 100644 index 14dfbdc..0000000 --- a/users/app/views/sessions/_admin_nav.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%a#admin-menu{"data-toggle" => "dropdown", :role => :button} - Admin -%ul.dropdown-menu{:role => "menu", "aria-labelledby" => "admin-menu"} - %li - = link_to Ticket.model_name.human(:count => ""), tickets_path, {:tabindex => -1} - = link_to User.model_name.human(:count => ""), users_path, {:tabindex => -1} diff --git a/users/app/views/sessions/_nav.html.haml b/users/app/views/sessions/_nav.html.haml deleted file mode 100644 index ac85bb5..0000000 --- a/users/app/views/sessions/_nav.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- if logged_in? - - if admin? - %li.dropdown - = render 'sessions/admin_nav' - %li - = link_to current_user.login, edit_user_path(current_user) - %li - = link_to t(:logout), logout_path, :method => :delete -- else - %li - = link_to t(:login), login_path - %li - = link_to t(:signup), signup_path diff --git a/users/app/views/sessions/new.html.haml b/users/app/views/sessions/new.html.haml index 6743407..c915968 100644 --- a/users/app/views/sessions/new.html.haml +++ b/users/app/views/sessions/new.html.haml @@ -1,9 +1,10 @@ -.span8.offset2 +.span1 +.span9 = render :partial => 'users/warnings' %h2=t :login = simple_form_for @session, :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| - %legend=t :login_message - = f.input :login, :input_html => { :id => :srp_username } - = f.input :password, :required => true, :input_html => { :id => :srp_password } - = f.button :submit, :value => t(:login), :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn
\ No newline at end of file + = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } + = f.input :password, :required => false, :input_html => { :id => :srp_password } + .form-actions + = f.button :submit, :value => t(:login), :class => 'btn-primary' + = link_to t(:cancel), root_path, :class => 'btn' diff --git a/users/app/views/users/_cancel_account.html.haml b/users/app/views/users/_cancel_account.html.haml deleted file mode 100644 index c5ab36a..0000000 --- a/users/app/views/users/_cancel_account.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%legend - - if @user == current_user - =t :cancel_account - %small You will not be able to login anymore. - - else - =t :admin_cancel_account, :username => @user.login -= link_to user_path(@user), :method => :delete, :confirm => t(:confirm_question), :class => "btn btn-danger" do - %i.icon-remove.icon-white - =t :remove_account diff --git a/users/app/views/users/_edit.html.haml b/users/app/views/users/_edit.html.haml new file mode 100644 index 0000000..adee8a4 --- /dev/null +++ b/users/app/views/users/_edit.html.haml @@ -0,0 +1,37 @@ +-# +-# edit user form, used by both show and edit actions. +-# + +-# +-# CHANGE PASSWORD +-# +-# * everything about this form is handled with javascript. So take care when changing any ids. +-# * the login is required when changing the password because it is used as part of the salt when calculating the password verifier. +-# however, we don't want the user to change their login without generating a new key, so we hide the ui for this +-# (although it works perfectly fine to change username if the field was visible). +-# +- form_options = {:url => '/not-used', :html => {:class => user_form_class('form-horizontal'), :id => 'update_login_and_password'}, :validate => true} += simple_form_for @user, form_options do |f| + %legend= t(:change_password) + = hidden_field_tag 'user_param', @user.to_param + .hidden + = f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username} + = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password } + = f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation } + .control-group + .controls + = f.submit t(:save), :class => 'btn btn-primary' + +-# +-# DESTROY ACCOUNT +-# + +%legend + - if @user == current_user + = t(:destroy_my_account) + - else + = t(:admin_destroy_account, :username => @user.login) +%p= t(:destroy_account_info) += link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do + %i.icon-remove.icon-white + = t(:destroy_my_account) diff --git a/users/app/views/users/_email_aliases.html.haml b/users/app/views/users/_email_aliases.html.haml deleted file mode 100644 index 6e32700..0000000 --- a/users/app/views/users/_email_aliases.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.span6 - %ul.unstyled - = render @user.email_aliases, :as => :li, :with => [:delete] -.clearfix -= f.simple_fields_for :email_aliases, @email_alias do |e| - = e.input :username, :placeholder => "alias" diff --git a/users/app/views/users/_email_field.html.haml b/users/app/views/users/_email_field.html.haml deleted file mode 100644 index edf62c9..0000000 --- a/users/app/views/users/_email_field.html.haml +++ /dev/null @@ -1 +0,0 @@ -= f.input :email, :placeholder => "me@#{APP_CONFIG[:domain]}" diff --git a/users/app/views/users/_email_forward_field.html.haml b/users/app/views/users/_email_forward_field.html.haml deleted file mode 100644 index 049428f..0000000 --- a/users/app/views/users/_email_forward_field.html.haml +++ /dev/null @@ -1 +0,0 @@ -= f.input :email_forward diff --git a/users/app/views/users/_form.html.haml b/users/app/views/users/_form.html.haml deleted file mode 100644 index cb51175..0000000 --- a/users/app/views/users/_form.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- only = local_assigns[:only] -- html = {:class => 'form-horizontal user form ' + (@user.new_record? ? 'new' : 'edit')} -= simple_form_for @user, :validate => true, :format => :json, :html => html do |f| - %legend - = t(only || :signup_message) - = yield - .pull-right - = f.button :submit - - unless only - = link_to t(:cancel), root_url, :class => :btn - .clearfix diff --git a/users/app/views/users/_legend_and_submit.html.haml b/users/app/views/users/_legend_and_submit.html.haml deleted file mode 100644 index 6fc0e4a..0000000 --- a/users/app/views/users/_legend_and_submit.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%legend= t(legend) -=yield -.pull-right= f.button :submit, :value => t(legend) -.clearfix diff --git a/users/app/views/users/_login_and_password_fields.html.haml b/users/app/views/users/_login_and_password_fields.html.haml deleted file mode 100644 index 0baefc7..0000000 --- a/users/app/views/users/_login_and_password_fields.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render :partial => 'login_field', :locals => {:f => f} -= render :partial => 'password_fields', :locals => {:f => f, :password_confirmation_hint => t(:can_retype_old_password)}
\ No newline at end of file diff --git a/users/app/views/users/_login_field.html.haml b/users/app/views/users/_login_field.html.haml deleted file mode 100644 index 8ab36c3..0000000 --- a/users/app/views/users/_login_field.html.haml +++ /dev/null @@ -1 +0,0 @@ -= f.input :login, :input_html => { :id => :srp_username } diff --git a/users/app/views/users/_password_fields.html.haml b/users/app/views/users/_password_fields.html.haml deleted file mode 100644 index 47b7b07..0000000 --- a/users/app/views/users/_password_fields.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= f.input :password, :required => true, :validate => true, :input_html => { :id => :srp_password } -= f.input :password_confirmation, :required => true, :hint => local_assigns[:password_confirmation_hint], :input_html => { :id => :srp_password_confirmation } diff --git a/users/app/views/users/_public_key_field.html.haml b/users/app/views/users/_public_key_field.html.haml deleted file mode 100644 index af88cbd..0000000 --- a/users/app/views/users/_public_key_field.html.haml +++ /dev/null @@ -1 +0,0 @@ -= f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "span5", :rows => 20} # will want to tweak this to be wide enough (maybe smaller text?) diff --git a/users/app/views/users/_user.html.haml b/users/app/views/users/_user.html.haml index ca03d34..990d9cf 100644 --- a/users/app/views/users/_user.html.haml +++ b/users/app/views/users/_user.html.haml @@ -1,10 +1,4 @@ %tr - %td= link_to user.login, user - %td= time_ago_in_words(user.created_at) + " ago" - %td - = link_to edit_user_path(user), :class => "btn btn-mini btn-primary" do - %i.icon-edit.icon-white - Edit - = link_to user_path(user), :method => :delete, :class => "btn btn-danger btn-mini" do - %i.icon-remove.icon-white - Remove + %td= link_to user.login, user_overview_path(user) + %td= l(user.created_at, :format => :short) + %td= l(user.updated_at, :format => :short) diff --git a/users/app/views/users/edit.html.haml b/users/app/views/users/edit.html.haml index 97bd48d..08e9dc3 100644 --- a/users/app/views/users/edit.html.haml +++ b/users/app/views/users/edit.html.haml @@ -1,17 +1 @@ -.span8.offset2 - %h2=t :settings - - tabs = [] - - content_for :account do - = user_form_with 'login_and_password_fields', :legend => :update_login_and_password if @user == current_user - = render 'cancel_account' - - tabs << :account - - if @user == current_user - - content_for :email do - %legend=t :email_address - =t :associated_email - = render @user.email_address, :as => :span - = user_form_with 'public_key_field', :legend => :public_key - = user_form_with 'email_forward_field', :legend => :forward_email - = user_form_with 'email_aliases', :legend => :add_email_alias - - tabs << :email - = render 'tabs/tabs', :tabs => tabs += render 'edit'
\ No newline at end of file diff --git a/users/app/views/users/index.html.haml b/users/app/views/users/index.html.haml index 9e6a179..fc1001e 100644 --- a/users/app/views/users/index.html.haml +++ b/users/app/views/users/index.html.haml @@ -1,17 +1,13 @@ -.page-header - %h1= User.model_name.human(:count =>User.count) -.row - .span8 - %h2= params[:query] ? "Users starting with '#{params[:query]}'" : "Last users who signed up" - %table.table.table-hover - %tr - %th Login - %th Created - %th Action - = render @users.all - .span4 - %h4 Find user - = form_tag users_path, :method => :get, :class => "form-search" do - .input-append - = text_field_tag :query, "", :class => "user typeahead span2 search-query", :autocomplete => :off - %button.btn{:type => :submit} Search +- @show_navigation = false + += form_tag users_path, :method => :get, :class => "form-search" do + .input-append + = text_field_tag :query, params[:query], :id => 'user-typeahead', :class => "search-query", :autocomplete => :off + %button.btn{:type => :submit}= t(:search) + +%table.table.table-striped + %tr + %th= t(:username) + %th= t(:created) + %th= t(:updated) + = render @users.all diff --git a/users/app/views/users/new.html.haml b/users/app/views/users/new.html.haml index 80482b2..f8d14b5 100644 --- a/users/app/views/users/new.html.haml +++ b/users/app/views/users/new.html.haml @@ -1,12 +1,19 @@ -.span8.offset2 +-# +-# This form is handled entirely by javascript, so take care when changing element ids. +-# + +- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} + +.span1 +.span9 = render :partial => 'warnings' %h2=t :signup - = user_form do |f| + = simple_form_for(@user, form_options) do |f| %legend= t(:signup_message) - = render :partial => 'login_field', :locals => {:f => f} - = render :partial => 'password_fields', :locals => {:f => f} - .pull-right - = f.button :submit, :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn - .clearfix + = f.input :login, :label => t(:username), :required => false, :input_html => { :id => :srp_username } + = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password } + = f.input :password_confirmation, :required => false, :validate => true, :input_html => { :id => :srp_password_confirmation } + .form-actions + = f.button :submit, :value => t(:signup), :class => 'btn btn-primary' + = link_to t(:cancel), root_url, :class => 'btn' diff --git a/users/app/views/users/show.html.haml b/users/app/views/users/show.html.haml index 056ed57..08e9dc3 100644 --- a/users/app/views/users/show.html.haml +++ b/users/app/views/users/show.html.haml @@ -1,31 +1 @@ -.span8.offset1 - %h2= @user.login - .small - = link_to 'edit', edit_user_path(@user) - %dl.offset1 - - fields = ['login', 'email_address', 'created_at', 'updated_at', 'email_forward'] - - fields.each do |field| - %dt - = field.titleize - %dd - = user_field(field) - %dt - =t :email_aliases - %dd - - aliases = @user.email_aliases - - if aliases.present? - %ul.pull-left.unstyled - = render aliases - - else - =t :none - .clearfix - %dt - =t :most_recently_updated_tickets - %dd - - tix = @user.most_recent_tickets - - if tix.present? - %table - %tbody - = render @user.most_recent_tickets - - else - =t :none
\ No newline at end of file += render 'edit'
\ No newline at end of file diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml index 32d183b..b880887 100644 --- a/users/config/locales/en.yml +++ b/users/config/locales/en.yml @@ -1,45 +1,63 @@ en: - none: "None." - signup: "Sign up" + email_settings: "Email Settings" + account_settings: "Account Settings" + logout: "Logout" + none: "None" + signup: "Sign Up" signup_message: "Please create an account." cancel: "Cancel" - login: "Login" - login_message: "Please login with your account." + login: "Log In" + username: "Username" + password: "Password" + change_password: "Change Password" + login_message: "Please log in with your account." invalid_user_pass: "Not a valid username/password combination" all_strategies_failed: "Could not understand your login attempt. Please first send your login and a SRP ephemeral value A and then send the client_auth in the same session (using cookies)." update_login_and_password: "Update Login and Password" - cancel_account: "Cancel your account" - remove_account: "Remove Account" - admin_cancel_account: "Cancel the account %{username}" + destroy_my_account: "Destroy my account" + destroy_account_info: "This will permanently destroy your account and all the data associated with it. Proceed with caution!" + admin_destroy_account: "Destroy the account %{username}" set_email_address: "Set email address" - forward_email: "Forward email" - email_aliases: "Email aliases" + forward_email: "Forward Email" + email_aliases: "Email Aliases" public_key: "Public Key" - add_email_alias: "Add email alias" - confirm_question: "Are you sure?" + add_email_alias: "Add Email Alias" user_updated_successfully: "Settings have been updated successfully." user_created_successfully: "Successfully created your account." - email_alias_destroyed_successfully: "Successfully removed the alias '%{alias}'." - use_ascii_key: "Use ASCII-armored PGP key" - can_retype_old_password: "Retype your old password if you would like to keep that" - associated_email: "The associated email address is" - cookie_disabled_warning: "You have cookies disabled. You will not be able to login until you enable cookies." - js_required: "We are sorry, but this doesn't work without javascript enabled. This is for security reasons." + email_alias_destroyed_successfully: "Removed email alias %{alias}." + use_ascii_key: "OpenPGP public key. Do not change this value unless you know what you are doing." + advanced_options: "Advanced Options" + not_authorized: "Sorry, but you are not authorized to perform that action." + not_authorized_login: "Please log in to perform that action." + search: "Search" + # + # overview + # + overview_welcome: "Welcome %{username}." + overview_intro: "From this user control panel, you can:" + overview_tickets: "Create and check support tickets." + overview_email: "Modify email settings." + overview_account: "Change your password or delete your account." + + # + # rails + # activemodel: models: - user: + user: one: User other: "%{count} Users" simple_form: labels: user: - email_forward: "Email forward" + email_forward: "Email Forward" hints: user: - email_forward: "Forward all emails to this address" - email: "Your leap web email address" + email_forward: > + Forward all email messages to this address. Messages will be encrypted before being forwarded. + This is an option for advanced users who are familar with OpenPGP. placeholders: user: email_forward: "my_other_email@domain.net" - + diff --git a/users/config/routes.rb b/users/config/routes.rb index 9a9a40e..b6d583e 100644 --- a/users/config/routes.rb +++ b/users/config/routes.rb @@ -5,7 +5,7 @@ Rails.application.routes.draw do defaults: {format: 'json'} } do resources :sessions, :only => [:new, :create, :update] delete "logout" => "sessions#destroy", :as => "logout" - resources :users, :only => [:create, :update] + resources :users, :only => [:create, :update, :destroy, :index] end get "login" => "sessions#new", :as => "login" @@ -13,7 +13,9 @@ Rails.application.routes.draw do resources :sessions, :only => [:new, :create, :update] get "signup" => "users#new", :as => "signup" - resources :users do + resources :users, :except => [:create, :update] do + resource :overview, :only => [:show] + resource :email_settings, :only => [:edit, :update] resources :email_aliases, :only => [:destroy], :id => /.*/ end diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index c57937f..013b44a 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -11,8 +11,8 @@ Gem::Specification.new do |s| s.homepage = "http://www.leap.se" s.summary = "User registration and authorization for the leap platform" s.description = "This this plugin for the leap platform provides user signup and login. It uses Secure Remote Password for the authentication." - - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"] + + s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile"] s.test_files = Dir["test/**/*"] s.add_dependency "leap_web_core", LeapWeb::VERSION diff --git a/users/test/factories.rb b/users/test/factories.rb index 6b094bd..777704b 100644 --- a/users/test/factories.rb +++ b/users/test/factories.rb @@ -13,7 +13,9 @@ FactoryGirl.define do end factory :admin_user do - is_admin? true + after(:build) do |admin| + admin.stubs(:is_admin?).returns(true) + end end end end diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb index 7f81c59..92a5f6c 100644 --- a/users/test/functional/users_controller_test.rb +++ b/users/test/functional/users_controller_test.rb @@ -79,33 +79,6 @@ class UsersControllerTest < ActionController::TestCase assert_redirected_to users_path end - test "should create new user" do - user_attribs = record_attributes_for :user - user = User.new(user_attribs) - User.expects(:create).with(user_attribs).returns(user) - - - post :create, :user => user_attribs, :format => :json - - - assert_nil session[:user_id] - assert_json_response user - assert_response :success - end - - test "should redirect to signup form on failed attempt" do - user_attribs = record_attributes_for :user - user_attribs.slice!('login') - user = User.new(user_attribs) - assert !user.valid? - User.expects(:create).with(user_attribs).returns(user) - - post :create, :user => user_attribs, :format => :json - - assert_json_error user.errors.messages - assert_response 422 - end - test "should get edit view" do user = find_record :user @@ -115,34 +88,6 @@ class UsersControllerTest < ActionController::TestCase assert_equal user, assigns[:user] end - test "user can change settings" do - user = find_record :user - changed_attribs = record_attributes_for :user_with_settings - user.expects(:attributes=).with(changed_attribs) - user.expects(:changed?).returns(true) - user.expects(:save).returns(true) - - login user - put :update, :user => changed_attribs, :id => user.id, :format => :json - - assert_equal user, assigns[:user] - assert_response 204 - assert_equal " ", @response.body - end - - # Eventually, admin will be able to update some user fields - test "admin cannot update user" do - user = find_record :user - changed_attribs = record_attributes_for :user_with_settings - - login :is_admin? => true - put :update, :user => changed_attribs, :id => user.id, :format => :json - - assert_response :redirect - assert_access_denied - - end - test "admin can destroy user" do user = find_record :user user.expects(:destroy) @@ -189,14 +134,6 @@ class UsersControllerTest < ActionController::TestCase assert_access_denied end - test "admin can autocomplete users" do - login :is_admin? => true - get :index, :format => :json - - assert_response :success - assert assigns(:users) - end - test "admin can search users" do login :is_admin? => true get :index, :query => "a" diff --git a/users/test/functional/v1/users_controller_test.rb b/users/test/functional/v1/users_controller_test.rb new file mode 100644 index 0000000..0d44e50 --- /dev/null +++ b/users/test/functional/v1/users_controller_test.rb @@ -0,0 +1,70 @@ +require 'test_helper' + +class V1::UsersControllerTest < ActionController::TestCase + + test "user can change settings" do + user = find_record :user + changed_attribs = record_attributes_for :user_with_settings + user.expects(:update_attributes).with(changed_attribs) + + login user + put :update, :user => changed_attribs, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can update user" do + user = find_record :user + changed_attribs = record_attributes_for :user_with_settings + user.expects(:update_attributes).with(changed_attribs) + + login :is_admin? => true + put :update, :user => changed_attribs, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + end + + test "user cannot update other user" do + user = find_record :user + login + put :update, :user => record_attributes_for(:user_with_settings), :id => user.id, :format => :json + assert_access_denied + end + + test "should create new user" do + user_attribs = record_attributes_for :user + user = User.new(user_attribs) + User.expects(:create).with(user_attribs).returns(user) + + post :create, :user => user_attribs, :format => :json + + assert_nil session[:user_id] + assert_json_response user + assert_response :success + end + + test "should redirect to signup form on failed attempt" do + user_attribs = record_attributes_for :user + user_attribs.slice!('login') + user = User.new(user_attribs) + assert !user.valid? + User.expects(:create).with(user_attribs).returns(user) + + post :create, :user => user_attribs, :format => :json + + assert_json_error user.errors.messages + assert_response 422 + end + + test "admin can autocomplete users" do + login :is_admin? => true + get :index, :query => 'a', :format => :json + + assert_response :success + assert assigns(:users) + end + +end diff --git a/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb index 1698105..2e45367 100644 --- a/users/test/integration/api/account_flow_test.rb +++ b/users/test/integration/api/account_flow_test.rb @@ -88,10 +88,11 @@ class AccountFlowTest < RackTest server_auth = @srp.authenticate(self) test_public_key = 'asdlfkjslfdkjasd' original_login = @user.login - put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => test_public_key, :login => 'failed_login_name'}, :format => :json + new_login = 'zaph' + put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => test_public_key, :login => new_login}, :format => :json @user.reload assert_equal test_public_key, @user.public_key - assert_equal original_login, @user.login + assert_equal new_login, @user.login # eventually probably want to remove most of this into a non-integration functional test # should not overwrite public key: put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:blee => :blah}, :format => :json diff --git a/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb index c0fcf3a..555b5db 100644 --- a/users/test/support/auth_test_helper.rb +++ b/users/test/support/auth_test_helper.rb @@ -20,10 +20,18 @@ module AuthTestHelper def assert_access_denied(denied = true, logged_in = true) if denied - assert_equal({:alert => "Not authorized"}, flash.to_hash) - # todo: eventually probably eliminate separate conditions - assert_redirected_to login_path if !logged_in - assert_redirected_to root_path if logged_in + if @response.content_type == 'application/json' + assert_json_response('error' => I18n.t(:not_authorized)) + assert_response :unprocessable_entity + else + if logged_in + assert_equal({:alert => I18n.t(:not_authorized)}, flash.to_hash) + assert_redirected_to root_url + else + assert_equal({:alert => I18n.t(:not_authorized_login)}, flash.to_hash) + assert_redirected_to login_url + end + end else assert flash[:alert].blank? end diff --git a/users/test/support/stub_record_helper.rb b/users/test/support/stub_record_helper.rb index 168a827..8aa1973 100644 --- a/users/test/support/stub_record_helper.rb +++ b/users/test/support/stub_record_helper.rb @@ -1,15 +1,18 @@ module StubRecordHelper - # Will expect find_by_param or find_by_id to be called on klass and + # + # We will stub find_by_param or find_by_id to be called on klass and # return the record given. + # # If no record is given but a hash or nil will create a stub based on # that instead and returns the stub. + # def find_record(factory, attribs_hash = {}) - attribs_hash.reverse_merge!(:id => Random.rand(10000).to_s) + attribs_hash = attribs_hash.reverse_merge(:id => Random.rand(10000).to_s) record = stub_record factory, attribs_hash klass = record.class finder = klass.respond_to?(:find_by_param) ? :find_by_param : :find - klass.expects(finder).with(record.to_param.to_s).returns(record) + klass.stubs(finder).with(record.to_param.to_s).returns(record) return record end |