summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjessib <jessib@leap.se>2013-07-09 11:53:58 -0700
committerjessib <jessib@leap.se>2013-07-09 11:53:58 -0700
commit09b7f01cac6df1ae11f4129b20b781b78a3706ac (patch)
tree3921eaa5edd03d80e6b402c5c2f88dda6338ab3a
parent9979b50848ce27730f880159512933e50d5ae0e4 (diff)
parent3113f8b814417a896ad5340fda88927733f8ab22 (diff)
Merge branch 'master' into feature/authentication_generic_error
Conflicts: app/views/layouts/_messages.html.haml app/views/layouts/application.html.haml users/app/assets/javascripts/users.js.coffee
-rw-r--r--.gitignore2
-rw-r--r--DEVELOP.md8
-rw-r--r--app/assets/javascripts/application.js3
-rw-r--r--app/assets/stylesheets/application.scss21
-rw-r--r--app/assets/stylesheets/leap.scss194
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/home_controller.rb3
-rw-r--r--app/helpers/application_helper.rb38
-rw-r--r--app/views/home/index.html.haml19
-rw-r--r--app/views/layouts/_content.html.haml19
-rw-r--r--app/views/layouts/_footer.html.haml (renamed from help/Readme.md)0
-rw-r--r--app/views/layouts/_header.html.haml11
-rw-r--r--app/views/layouts/_masthead.html.haml4
-rw-r--r--app/views/layouts/_messages.html.haml11
-rw-r--r--app/views/layouts/_navigation.html.haml12
-rw-r--r--app/views/layouts/application.html.haml26
-rw-r--r--common_dependencies.rb2
-rw-r--r--config/application.rb7
-rw-r--r--config/defaults.yml9
-rw-r--r--config/initializers/i18n.rb2
-rw-r--r--config/locales/en.yml5
-rw-r--r--core/app/assets/javascripts/leap.js7
-rw-r--r--core/app/helpers/core_helper.rb13
-rw-r--r--core/app/helpers/navigation_helper.rb82
-rw-r--r--core/app/views/common/_home_page_buttons.html.haml19
-rw-r--r--core/app/views/kaminari/_first_page.html.haml9
-rw-r--r--core/app/views/kaminari/_gap.html.haml8
-rw-r--r--core/app/views/kaminari/_last_page.html.haml9
-rw-r--r--core/app/views/kaminari/_next_page.html.haml12
-rw-r--r--core/app/views/kaminari/_page.html.haml14
-rw-r--r--core/app/views/kaminari/_paginator.html.haml19
-rw-r--r--core/app/views/kaminari/_prev_page.html.haml12
-rw-r--r--core/config/initializers/simple_form_bootstrap.rb12
-rw-r--r--core/config/locales/en.yml30
-rw-r--r--help/README.md1
-rw-r--r--help/app/assets/javascripts/tickets.js8
-rw-r--r--help/app/controllers/tickets_controller.rb146
-rw-r--r--help/app/helpers/auto_tickets_path_helper.rb51
-rw-r--r--help/app/helpers/tickets_helper.rb57
-rw-r--r--help/app/models/ticket.rb106
-rw-r--r--help/app/models/ticket_selection.rb41
-rw-r--r--help/app/views/tickets/_admin-nav.html.haml5
-rw-r--r--help/app/views/tickets/_comment.html.haml34
-rw-r--r--help/app/views/tickets/_edit_form.html.haml48
-rw-r--r--help/app/views/tickets/_new_comment.html.haml4
-rw-r--r--help/app/views/tickets/_new_comment_form.html.haml13
-rw-r--r--help/app/views/tickets/_order-nav.html.haml5
-rw-r--r--help/app/views/tickets/_status-nav.html.haml7
-rw-r--r--help/app/views/tickets/_table-nav.html.haml5
-rw-r--r--help/app/views/tickets/_tabs.html.haml23
-rw-r--r--help/app/views/tickets/_ticket.html.haml21
-rw-r--r--help/app/views/tickets/_ticket_data.html.haml35
-rw-r--r--help/app/views/tickets/index.html.haml30
-rw-r--r--help/app/views/tickets/new.html.haml38
-rw-r--r--help/app/views/tickets/show.html.haml28
-rw-r--r--help/config/locales/en.yml22
-rw-r--r--help/config/routes.rb7
-rw-r--r--help/leap_web_help.gemspec2
-rw-r--r--help/test/functional/tickets_controller_test.rb13
-rw-r--r--public/leap-img/32/arrow-down.pngbin0 -> 453 bytes
-rw-r--r--ui_dependencies.rb10
-rw-r--r--users/README.rdoc3
m---------users/app/assets/javascripts/srp0
-rw-r--r--users/app/assets/javascripts/users.js78
-rw-r--r--users/app/assets/javascripts/users.js.coffee63
-rw-r--r--users/app/controllers/account_settings_controller.rb (renamed from users/Readme.md)0
-rw-r--r--users/app/controllers/controller_extension/authentication.rb15
-rw-r--r--users/app/controllers/email_aliases_controller.rb18
-rw-r--r--users/app/controllers/email_settings_controller.rb41
-rw-r--r--users/app/controllers/overviews_controller.rb9
-rw-r--r--users/app/controllers/sessions_controller.rb11
-rw-r--r--users/app/controllers/users_base_controller.rb18
-rw-r--r--users/app/controllers/users_controller.rb64
-rw-r--r--users/app/controllers/v1/users_controller.rb20
-rw-r--r--users/app/helpers/users_helper.rb36
-rw-r--r--users/app/models/user.rb1
-rw-r--r--users/app/views/email_settings/edit.html.haml38
-rw-r--r--users/app/views/emails/_email.html.haml2
-rw-r--r--users/app/views/overviews/show.html.haml18
-rw-r--r--users/app/views/sessions/_admin_nav.html.haml6
-rw-r--r--users/app/views/sessions/_nav.html.haml13
-rw-r--r--users/app/views/sessions/new.html.haml13
-rw-r--r--users/app/views/users/_cancel_account.html.haml9
-rw-r--r--users/app/views/users/_edit.html.haml37
-rw-r--r--users/app/views/users/_email_aliases.html.haml6
-rw-r--r--users/app/views/users/_email_field.html.haml1
-rw-r--r--users/app/views/users/_email_forward_field.html.haml1
-rw-r--r--users/app/views/users/_form.html.haml11
-rw-r--r--users/app/views/users/_legend_and_submit.html.haml4
-rw-r--r--users/app/views/users/_login_and_password_fields.html.haml2
-rw-r--r--users/app/views/users/_login_field.html.haml1
-rw-r--r--users/app/views/users/_password_fields.html.haml2
-rw-r--r--users/app/views/users/_public_key_field.html.haml1
-rw-r--r--users/app/views/users/_user.html.haml12
-rw-r--r--users/app/views/users/edit.html.haml18
-rw-r--r--users/app/views/users/index.html.haml30
-rw-r--r--users/app/views/users/new.html.haml23
-rw-r--r--users/app/views/users/show.html.haml32
-rw-r--r--users/config/locales/en.yml61
-rw-r--r--users/config/routes.rb6
-rw-r--r--users/leap_web_users.gemspec4
-rw-r--r--users/test/factories.rb4
-rw-r--r--users/test/functional/users_controller_test.rb65
-rw-r--r--users/test/functional/v1/users_controller_test.rb70
-rw-r--r--users/test/integration/api/account_flow_test.rb5
-rw-r--r--users/test/support/auth_test_helper.rb16
-rw-r--r--users/test/support/stub_record_helper.rb9
107 files changed, 1533 insertions, 778 deletions
diff --git a/.gitignore b/.gitignore
index 73cd22e..a79d841 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,5 @@ public/provider.json
config/config.yml
bin
.*.swp
+public/1/*
+vendor/bundle/*
diff --git a/DEVELOP.md b/DEVELOP.md
index 9548774..e19b827 100644
--- a/DEVELOP.md
+++ b/DEVELOP.md
@@ -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 3e1b42c..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: 40px;
-}
@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..b382773
--- /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(/leap-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/_messages.html.haml b/app/views/layouts/_messages.html.haml
index 6ec70a2..3a2361e 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, :class => "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}" \ No newline at end of file
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 2b8206d..380f92d 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,23 +1,25 @@
+- @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 params[:controller] != 'home'
+ = render 'layouts/masthead'
+ #main
+ .container-fluid
+ - if logged_in?
+ .row-fluid
.span12
- #messages= 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..25b377a
--- /dev/null
+++ b/core/config/locales/en.yml
@@ -0,0 +1,30 @@
+en:
+ no_such_thing: "No such %{thing}."
+ thing_was_successfully_created: "%{thing} was successfully created."
+ create_thing: "Create %{thing}"
+
+ 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..c0a343d 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', :value => t(:create_thing, :thing => t(:ticket))
+ - 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..342adea 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/leap-img/32/arrow-down.png b/public/leap-img/32/arrow-down.png
new file mode 100644
index 0000000..3b16e6d
--- /dev/null
+++ b/public/leap-img/32/arrow-down.png
Binary files differ
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/srp b/users/app/assets/javascripts/srp
-Subproject e7a0b830b8f994316a560001a9e7397422b184b
+Subproject 926a5d5960db51903e33c8496487da59f9f4124
diff --git a/users/app/assets/javascripts/users.js b/users/app/assets/javascripts/users.js
new file mode 100644
index 0000000..4195df8
--- /dev/null
+++ b/users/app/assets/javascripts/users.js
@@ -0,0 +1,78 @@
+(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) {
+ if (field == 'base') {
+ alert_message(message.errors[field]);
+ next;
+ }
+ 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 c9cc182..0000000
--- a/users/app/assets/javascripts/users.js.coffee
+++ /dev/null
@@ -1,63 +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
- display_errors(message.errors)
- else
- alert(message)
-
-display_errors = (errors) ->
- clear_errors();
- for field, error of errors
- if field == 'base'
- display_base_error(error);
- else
- display_field_error(field, error);
-
-clear_errors = ->
- $('#messages').empty();
-
-display_field_error = (field, error) ->
- element = $('form input[name$="['+field+']"]')
- return unless element
- element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false)
-
-display_base_error = (message) ->
- messages = $('#messages')
- messages.append "<div class=\"alert alert-error\">" + message + "</div></div>"
-
-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 0dca29c..6daffdb 100644
--- a/users/app/controllers/controller_extension/authentication.rb
+++ b/users/app/controllers/controller_extension/authentication.rb
@@ -39,9 +39,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 dff1ed5..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 : login_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 756170b..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, :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 dded88c..b880887 100644
--- a/users/config/locales/en.yml
+++ b/users/config/locales/en.yml
@@ -1,44 +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"
+ 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 fd8869a..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)
@@ -162,7 +107,7 @@ class UsersControllerTest < ActionController::TestCase
delete :destroy, :id => @current_user.id
assert_response :redirect
- assert_redirected_to login_path
+ assert_redirected_to root_path
end
test "non-admin can't destroy user" do
@@ -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