summaryrefslogtreecommitdiff
path: root/engines/support/app
diff options
context:
space:
mode:
Diffstat (limited to 'engines/support/app')
-rw-r--r--engines/support/app/assets/javascripts/tickets.js4
-rw-r--r--engines/support/app/controllers/tickets_controller.rb161
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by.js13
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js12
-rw-r--r--engines/support/app/helpers/auto_tickets_path_helper.rb54
-rw-r--r--engines/support/app/helpers/tickets_helper.rb76
-rw-r--r--engines/support/app/models/account_extension/tickets.rb13
-rw-r--r--engines/support/app/models/ticket.rb112
-rw-r--r--engines/support/app/models/ticket_comment.rb43
-rw-r--r--engines/support/app/models/ticket_selection.rb71
-rw-r--r--engines/support/app/views/tickets/_comment.html.haml20
-rw-r--r--engines/support/app/views/tickets/_edit_form.html.haml50
-rw-r--r--engines/support/app/views/tickets/_new_comment_form.html.haml13
-rw-r--r--engines/support/app/views/tickets/_tabs.html.haml23
-rw-r--r--engines/support/app/views/tickets/_ticket.html.haml6
-rw-r--r--engines/support/app/views/tickets/index.html.haml19
-rw-r--r--engines/support/app/views/tickets/new.html.haml17
-rw-r--r--engines/support/app/views/tickets/show.html.haml12
21 files changed, 755 insertions, 0 deletions
diff --git a/engines/support/app/assets/javascripts/tickets.js b/engines/support/app/assets/javascripts/tickets.js
new file mode 100644
index 0000000..18537aa
--- /dev/null
+++ b/engines/support/app/assets/javascripts/tickets.js
@@ -0,0 +1,4 @@
+//$(document).ready(function () {
+// $.fn.editable.defaults.mode = 'inline';
+// $('#subject').editable();
+//}); \ No newline at end of file
diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb
new file mode 100644
index 0000000..99357ab
--- /dev/null
+++ b/engines/support/app/controllers/tickets_controller.rb
@@ -0,0 +1,161 @@
+class TicketsController < ApplicationController
+ include AutoTicketsPathHelper
+
+ respond_to :html, :json
+ #has_scope :open, :type => boolean
+
+ before_filter :require_login, :only => [:index]
+ before_filter :fetch_ticket, :only => [:show, :update, :destroy]
+ before_filter :require_ticket_access, :only => [:show, :update, :destroy]
+ before_filter :fetch_user
+ before_filter :set_title
+
+ def new
+ @ticket = Ticket.new
+ @ticket.created_by = current_user.id
+ @ticket.comments.build
+ end
+
+ def create
+ @ticket = Ticket.new(params[:ticket])
+
+ #protecting posted_by isn't working, so this should protect it:
+ @ticket.comments.last.posted_by = current_user.id
+ @ticket.comments.last.private = false unless admin?
+ @ticket.created_by = current_user.id
+ if @ticket.save
+ flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket))
+ if !logged_in?
+ flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id))
+ end
+ end
+ respond_with(@ticket, :location => auto_ticket_path(@ticket))
+ end
+
+ def show
+ @comment = TicketComment.new
+ if !@ticket
+ redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => t(:ticket))
+ return
+ end
+ end
+
+ def update
+ if params[:button] == 'close'
+ @ticket.is_open = false
+ @ticket.save
+ redirect_to_tickets
+ elsif params[:button] == 'open'
+ @ticket.is_open = true
+ @ticket.save
+ redirect_to auto_ticket_path(@ticket)
+ else
+ @ticket.attributes = cleanup_ticket_params(params[:ticket])
+
+ if params[:button] == 'reply_and_close'
+ @ticket.close
+ end
+
+ if @ticket.comments_changed?
+ @ticket.comments.last.posted_by = current_user.id
+ @ticket.comments.last.private = false unless admin?
+ end
+
+ if @ticket.changed? and @ticket.save
+ flash[:notice] = t(:changes_saved)
+ redirect_to_tickets
+ else
+ flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed?
+ redirect_to auto_ticket_path(@ticket)
+ end
+ end
+ end
+
+ def index
+ @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 auto_tickets_path
+ end
+
+ protected
+
+ def set_title
+ @title = t(:tickets)
+ end
+
+ private
+
+ #
+ # redirects to ticket index, if appropriate.
+ # otherwise, just redirects to @ticket
+ #
+ def redirect_to_tickets
+ if logged_in?
+ if params[:button] == 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
+
+ #
+ # 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 fetch_ticket
+ @ticket = Ticket.find(params[:id])
+ if !@ticket
+ if admin?
+ redirect_to auto_tickets_path,
+ alert: t(:no_such_thing, thing: 'ticket')
+ else
+ access_denied
+ end
+ end
+ end
+
+ def require_ticket_access
+ access_denied unless ticket_access?
+ end
+
+ def ticket_access?
+ admin? or
+ @ticket.created_by.blank? or
+ current_user.id == @ticket.created_by
+ end
+
+ def fetch_user
+ if params[:user_id]
+ @user = User.find(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
diff --git a/engines/support/app/designs/ticket/by_includes_post_by.js b/engines/support/app/designs/ticket/by_includes_post_by.js
new file mode 100644
index 0000000..2eeac89
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by.js
@@ -0,0 +1,13 @@
+// TODO: This view is only used in tests--should we keep it?
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit(comment.posted_by, 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js
new file mode 100644
index 0000000..72169b0
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.created_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js
new file mode 100644
index 0000000..33dfe0b
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.is_open, doc.created_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js
new file mode 100644
index 0000000..3bd2a74
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.is_open, doc.updated_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js
new file mode 100644
index 0000000..2b4304f
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.updated_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/helpers/auto_tickets_path_helper.rb b/engines/support/app/helpers/auto_tickets_path_helper.rb
new file mode 100644
index 0000000..5638222
--- /dev/null
+++ b/engines/support/app/helpers/auto_tickets_path_helper.rb
@@ -0,0 +1,54 @@
+#
+# 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={})
+ return unless options.class == Hash
+ 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={})
+ return unless ticket.persisted?
+ 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={})
+ return unless options.class == Hash
+ 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
diff --git a/engines/support/app/helpers/tickets_helper.rb b/engines/support/app/helpers/tickets_helper.rb
new file mode 100644
index 0000000..7af50d6
--- /dev/null
+++ b/engines/support/app/helpers/tickets_helper.rb
@@ -0,0 +1,76 @@
+module TicketsHelper
+ #
+ # FORM HELPERS
+ #
+
+ #
+ # 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
+
+ #
+ # PARAM HELPERS
+ #
+
+ def search_status
+ if action?(:index)
+ params[:open_status] || 'open'
+ else
+ nil
+ end
+ end
+
+ def search_order
+ params[:sort_order] || 'updated_at_desc'
+ end
+
+ #
+ # LINK HELPERS
+ #
+
+ def link_to_status(new_status)
+ 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 search_order.start_with?(order_field)
+ # link for currently-filtered field. Link to other direction of this field.
+ if search_order.end_with? 'asc'
+ direction = 'desc'
+ icon_direction = 'up'
+ else
+ direction = 'asc'
+ icon_direction = 'down'
+ end
+ arrow = content_tag(:i, '', class: 'icon-arrow-'+ icon_direction)
+ else
+ # for not-currently-filtered field, don't display an arrow, and link to descending direction
+ arrow = ''
+ direction = 'desc'
+ end
+
+ 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
+
+end
diff --git a/engines/support/app/models/account_extension/tickets.rb b/engines/support/app/models/account_extension/tickets.rb
new file mode 100644
index 0000000..f898b56
--- /dev/null
+++ b/engines/support/app/models/account_extension/tickets.rb
@@ -0,0 +1,13 @@
+module AccountExtension::Tickets
+ extend ActiveSupport::Concern
+
+ def destroy_with_tickets
+ Ticket.destroy_all_from(self.user)
+ destroy_without_tickets
+ end
+
+ included do
+ alias_method_chain :destroy, :tickets
+ end
+
+end
diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb
new file mode 100644
index 0000000..bf5df53
--- /dev/null
+++ b/engines/support/app/models/ticket.rb
@@ -0,0 +1,112 @@
+#
+# 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
+ use_database "tickets"
+
+ 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 :subject, String
+ property :email, String
+ property :is_open, TrueClass, :default => true
+ property :comments, [TicketComment]
+
+ timestamps!
+
+ design do
+ view :by_updated_at
+ view :by_created_at
+ view :by_created_by
+
+ view :by_is_open_and_created_at
+ view :by_is_open_and_updated_at
+
+ own_path = Pathname.new(File.dirname(__FILE__))
+ load_views(own_path.join('..', 'designs', 'ticket'))
+ end
+
+ validates :subject, :presence => true
+
+ # email can have three states:
+ # * nil - prefilled with created_by's email
+ # * "" - cleared
+ # * valid email address
+ validates :email, :allow_blank => true, :format => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/
+
+ def self.search(options = {})
+ @selection = TicketSelection.new(options)
+ @selection.tickets
+ end
+
+ def self.destroy_all_from(user)
+ self.by_created_by.key(user.id).each do |ticket|
+ ticket.destroy
+ end
+ end
+
+ def is_creator_validated?
+ created_by_user.is_a? User
+ end
+
+ def email
+ read_attribute(:email) || created_by_user.email
+ end
+
+ def regarding_user
+ read_attribute(:regarding_user) || created_by_user.login
+ end
+
+ def close
+ self.is_open = false
+ end
+
+ def reopen
+ self.is_open = true
+ end
+
+ def commenters
+ commenters = []
+ self.comments.each do |comment|
+ if comment.posted_by
+ 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')
+ end
+ else
+ 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
+ comment = TicketComment.new(attributes.values.first)
+ comment.posted_at = Time.now
+ comments << comment
+ end
+ end
+
+ def created_by_user
+ if self.created_by
+ User.find(self.created_by) || AnonymousUser.new
+ else
+ AnonymousUser.new
+ end
+ end
+
+ def regarding_user_actual_user
+ User.find_by_login(self.regarding_user)
+ end
+
+end
diff --git a/engines/support/app/models/ticket_comment.rb b/engines/support/app/models/ticket_comment.rb
new file mode 100644
index 0000000..bed5237
--- /dev/null
+++ b/engines/support/app/models/ticket_comment.rb
@@ -0,0 +1,43 @@
+class TicketComment
+ include CouchRest::Model::Embeddable
+
+ #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way?
+ property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this.
+ # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL
+ property :posted_at, Time#, :protected => true
+ #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created
+ property :body, String
+ property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin
+
+ # ? timestamps!
+ validates :body, :presence => true
+ #before_validation :set_time#, :set_posted_by
+
+ #design do
+ # view :by_posted_at
+ # view :by_body
+ #end
+
+ def is_comment_validated?
+ !!posted_by
+ end
+
+ def posted_by_user
+ User.find(posted_by) if posted_by
+ end
+
+=begin
+ #TODO.
+ #this is resetting all comments associated with the ticket:
+ def set_time
+ self.posted_at = Time.now
+ end
+=end
+
+=begin
+ def set_posted_by
+ self.posted_by = User.current if User.current
+ end
+=end
+
+end
diff --git a/engines/support/app/models/ticket_selection.rb b/engines/support/app/models/ticket_selection.rb
new file mode 100644
index 0000000..74d5b78
--- /dev/null
+++ b/engines/support/app/models/ticket_selection.rb
@@ -0,0 +1,71 @@
+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 = {})
+ @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
+ Ticket.send(finder_method).startkey(startkey).endkey(endkey).send(order)
+ end
+
+ 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 @open_status != 'all'
+ method += @sort_order.sub(/_(de|a)sc$/, '')
+ end
+
+ def startkey
+ startkeys = []
+ 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
+ end
+
+ def endkey
+ endtime = Time.now + 2.days # TODO. this obviously isn't ideal
+ if self.startkey.is_a?(Array)
+ endkeys = self.startkey
+ endkeys.pop
+ endkeys << endtime
+ else
+ endtime
+ end
+ end
+
+ def order
+ # we have defined the ascending method to return the view itself:
+ (@sort_order.end_with? 'desc') ? 'descending' : 'ascending'
+ end
+
+
+ def only_mine?
+ !@is_admin || @admin_status == 'mine'
+ end
+
+end
diff --git a/engines/support/app/views/tickets/_comment.html.haml b/engines/support/app/views/tickets/_comment.html.haml
new file mode 100644
index 0000000..778ca13
--- /dev/null
+++ b/engines/support/app/views/tickets/_comment.html.haml
@@ -0,0 +1,20 @@
+- if admin? or !comment.private # only show comment if user is admin or comment is not private
+ %tr
+ %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
+ %div
+ %span.label.label-important
+ = t(:private)
+ %td.comment
+ = simple_format(comment.body) \ No newline at end of file
diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml
new file mode 100644
index 0000000..b8da779
--- /dev/null
+++ b/engines/support/app/views/tickets/_edit_form.html.haml
@@ -0,0 +1,50 @@
+:ruby
+ # created by user link
+ if @ticket.is_creator_validated?
+ created_by = link_to @ticket.created_by_user.login, @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, @ticket.regarding_user_actual_user
+ else
+ regarding_user_link = "(#{t(:unknown)})"
+ end
+ else
+ regarding_user_link = ''
+ end
+
+= simple_form_for @ticket do |f|
+ = hidden_ticket_fields
+ %p.first
+ - if @ticket.is_open?
+ %span.label.label-info
+ %b{style: 'padding:10px'}= t(:open)
+ = f.button :loading, t(:close), value: 'close', class: 'btn-mini'
+ - else
+ %span.label.label-success
+ %b{style: 'padding:10px'}= t(:closed)
+ = f.button :loading, t(:open), value: 'open', class: 'btn-mini'
+ %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe
+= simple_form_for @ticket do |f|
+ = hidden_ticket_fields
+ %div= t(:subject)
+ = f.text_field :subject, :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 :loading, t(:save), :value => 'save'
+ - if admin?
+ = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn'
diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml
new file mode 100644
index 0000000..40c737f
--- /dev/null
+++ b/engines/support/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 :loading, t(:post_reply), class: 'btn-primary', value: 'post_reply'
+ - if logged_in? && @ticket.is_open
+ = f.button :loading, t(:reply_and_close), value: 'reply_and_close'
+ = link_to t(:cancel), auto_tickets_path, :class => :btn
diff --git a/engines/support/app/views/tickets/_tabs.html.haml b/engines/support/app/views/tickets/_tabs.html.haml
new file mode 100644
index 0000000..445a909
--- /dev/null
+++ b/engines/support/app/views/tickets/_tabs.html.haml
@@ -0,0 +1,23 @@
+-#
+-# SORT ORDER TABS
+-#
+- unless action?(:new) or action?(:create)
+ %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) || action?(:create))}
+ = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path
diff --git a/engines/support/app/views/tickets/_ticket.html.haml b/engines/support/app/views/tickets/_ticket.html.haml
new file mode 100644
index 0000000..5bc33c8
--- /dev/null
+++ b/engines/support/app/views/tickets/_ticket.html.haml
@@ -0,0 +1,6 @@
+- url = auto_ticket_path(ticket)
+%tr
+ %td= link_to ticket.subject, 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/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml
new file mode 100644
index 0000000..a4df6e3
--- /dev/null
+++ b/engines/support/app/views/tickets/index.html.haml
@@ -0,0 +1,19 @@
+- @show_navigation = params[:user_id].present?
+
+= render 'tickets/tabs'
+
+%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/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml
new file mode 100644
index 0000000..3de5fe9
--- /dev/null
+++ b/engines/support/app/views/tickets/new.html.haml
@@ -0,0 +1,17 @@
+- @show_navigation = params[:user_id].present?
+
+= render 'tickets/tabs'
+
+- user = @user if admin?
+- user ||= current_user
+
+= simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f|
+ = hidden_ticket_fields
+ = f.input :subject
+ = f.input :email
+ = f.input :regarding_user
+ = 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
+ = f.button :wrapped, cancel: (logged_in? ? auto_tickets_path : home_path)
diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml
new file mode 100644
index 0000000..4f3c127
--- /dev/null
+++ b/engines/support/app/views/tickets/show.html.haml
@@ -0,0 +1,12 @@
+- @show_navigation = params[:user_id].present?
+
+.ticket
+ = render 'tickets/edit_form'
+ %table.table.table-striped.table-bordered
+ %tbody
+ = render :partial => 'tickets/comment', :collection => @ticket.comments
+ %tr
+ %td.user
+ = current_user.login || t(:anonymous)
+ %td.comment
+ = render 'tickets/new_comment_form'