Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

In the beginning...

  • Loading branch information...
commit 50ddae891b765a0372487739965e200565678c21 0 parents
Lee Smith authored
Showing with 13,553 additions and 0 deletions.
  1. +9 −0 .gitignore
  2. +21 −0 LICENSE
  3. +70 −0 README.rdoc
  4. +10 −0 Rakefile
  5. +154 −0 app/controllers/admin_controller.rb
  6. +32 −0 app/controllers/alerts_controller.rb
  7. +87 −0 app/controllers/application_controller.rb
  8. +54 −0 app/controllers/attachments_controller.rb
  9. +48 −0 app/controllers/comments_controller.rb
  10. +115 −0 app/controllers/contacts_controller.rb
  11. +31 −0 app/controllers/dashboard_controller.rb
  12. +50 −0 app/controllers/password_resets_controller.rb
  13. +144 −0 app/controllers/tickets_controller.rb
  14. +34 −0 app/controllers/user_sessions_controller.rb
  15. +117 −0 app/controllers/users_controller.rb
  16. +2 −0  app/helpers/admin_helper.rb
  17. +2 −0  app/helpers/alerts_helper.rb
  18. +25 −0 app/helpers/application_helper.rb
  19. +2 −0  app/helpers/attachments_helper.rb
  20. +2 −0  app/helpers/comments_helper.rb
  21. +2 −0  app/helpers/contacts_helper.rb
  22. +2 −0  app/helpers/dashboard_helper.rb
  23. +2 −0  app/helpers/password_resets_helper.rb
  24. +57 −0 app/helpers/tickets_helper.rb
  25. +2 −0  app/helpers/user_sessions_helper.rb
  26. +2 −0  app/helpers/users_helper.rb
  27. +10 −0 app/models/alert.rb
  28. +64 −0 app/models/attachment.rb
  29. +14 −0 app/models/attachment_observer.rb
  30. +62 −0 app/models/audit_sweeper.rb
  31. +10 −0 app/models/comment.rb
  32. +24 −0 app/models/contact.rb
  33. +18 −0 app/models/group.rb
  34. +27 −0 app/models/notifier.rb
  35. +23 −0 app/models/priority.rb
  36. +18 −0 app/models/status.rb
  37. +55 −0 app/models/ticket.rb
  38. +11 −0 app/models/ticket_observer.rb
  39. +53 −0 app/models/user.rb
  40. +12 −0 app/models/user_session.rb
  41. +156 −0 app/views/admin/index.html.erb
  42. +8 −0 app/views/attachments/_attachment.html.erb
  43. +11 −0 app/views/comments/_comment.html.erb
  44. +10 −0 app/views/contacts/_alphabet.html.erb
  45. +19 −0 app/views/contacts/_contacts.html.erb
  46. +20 −0 app/views/contacts/_form.html.erb
  47. +9 −0 app/views/contacts/edit.html.erb
  48. +18 −0 app/views/contacts/index.html.erb
  49. +1 −0  app/views/contacts/index.js.erb
  50. +9 −0 app/views/contacts/new.html.erb
  51. +34 −0 app/views/contacts/show.html.erb
  52. +102 −0 app/views/dashboard/index.html.erb
  53. +74 −0 app/views/layouts/application.html.erb
  54. +36 −0 app/views/layouts/user_sessions.html.erb
  55. +18 −0 app/views/notifier/owner_alert.text.html.erb
  56. +5 −0 app/views/notifier/password_reset_instructions.text.plain.erb
  57. +16 −0 app/views/notifier/ticket_alert.text.html.erb
  58. +17 −0 app/views/password_resets/edit.html.erb
  59. +15 −0 app/views/password_resets/new.html.erb
  60. +34 −0 app/views/tickets/_form.html.erb
  61. +94 −0 app/views/tickets/_ticket_filter.html.erb
  62. +37 −0 app/views/tickets/_tickets.html.erb
  63. +4 −0 app/views/tickets/_tickets_per_page.html.erb
  64. +3 −0  app/views/tickets/edit.html.erb
  65. +25 −0 app/views/tickets/index.html.erb
  66. +1 −0  app/views/tickets/index.js.erb
  67. +3 −0  app/views/tickets/new.html.erb
  68. +89 −0 app/views/tickets/show.html.erb
  69. +54 −0 app/views/tickets/show.pdf.prawn
  70. +18 −0 app/views/user_sessions/new.html.erb
  71. +27 −0 app/views/users/_form.erb
  72. +23 −0 app/views/users/_users.html.erb
  73. +9 −0 app/views/users/edit.html.erb
  74. +18 −0 app/views/users/index.html.erb
  75. +1 −0  app/views/users/index.js.erb
  76. +12 −0 app/views/users/new.html.erb
  77. +85 −0 app/views/users/show.html.erb
  78. +110 −0 config/boot.rb
  79. +20 −0 config/config.yml
  80. +22 −0 config/database.example.yml
  81. +49 −0 config/environment.rb
  82. +17 −0 config/environments/development.rb
  83. +28 −0 config/environments/production.rb
  84. +28 −0 config/environments/test.rb
  85. +7 −0 config/initializers/backtrace_silencers.rb
  86. +10 −0 config/initializers/inflections.rb
  87. +2 −0  config/initializers/load_config.rb
  88. +5 −0 config/initializers/mime_types.rb
  89. +21 −0 config/initializers/new_rails_defaults.rb
  90. +3 −0  config/initializers/paperclip.rb
  91. +15 −0 config/initializers/session_store.rb
  92. +5 −0 config/locales/en.yml
  93. +83 −0 config/routes.rb
  94. +28 −0 db/migrate/20081129214804_create_tickets.rb
  95. +12 −0 db/migrate/20081130035032_create_groups.rb
  96. +12 −0 db/migrate/20081130035501_create_statuses.rb
  97. +12 −0 db/migrate/20081130035552_create_priorities.rb
  98. +18 −0 db/migrate/20081205024712_create_comments.rb
  99. +35 −0 db/migrate/20081228061919_create_users.rb
  100. +19 −0 db/migrate/20090513023515_create_contacts.rb
  101. +20 −0 db/migrate/20090618031802_create_attachments.rb
  102. +17 −0 db/migrate/20090914141258_create_alerts.rb
  103. +129 −0 db/schema.rb
  104. +43 −0 db/seeds.rb
  105. +2 −0  doc/README_FOR_APP
  106. +80 −0 lib/tasks/faker.rake
  107. +40 −0 public/404.html
  108. +40 −0 public/422.html
  109. +41 −0 public/500.html
  110. BIN  public/favicon.ico
  111. BIN  public/images/accept.png
  112. BIN  public/images/add-alert.png
  113. BIN  public/images/add-attachment.png
  114. BIN  public/images/add-comment.png
  115. BIN  public/images/add.png
  116. BIN  public/images/avatar.gif
  117. BIN  public/images/back-arrow.png
  118. BIN  public/images/balloon.png
  119. BIN  public/images/bullet_black.png
  120. BIN  public/images/bullet_blue.png
  121. BIN  public/images/bullet_red.png
  122. BIN  public/images/bullet_yellow.png
  123. BIN  public/images/button-overlay.png
  124. BIN  public/images/calendar.png
  125. BIN  public/images/delete-alert.png
  126. BIN  public/images/delete.png
  127. BIN  public/images/disable.png
  128. BIN  public/images/document-excel.png
  129. BIN  public/images/document-film.png
  130. BIN  public/images/document-image.png
  131. BIN  public/images/document-music.png
  132. BIN  public/images/document-pdf.png
  133. BIN  public/images/document-powerpoint.png
  134. BIN  public/images/document-text.png
  135. BIN  public/images/document-word.png
  136. BIN  public/images/document-zipper.png
  137. BIN  public/images/document.png
  138. BIN  public/images/edit-contact.png
  139. BIN  public/images/edit-ticket.png
  140. BIN  public/images/edit-user.png
  141. BIN  public/images/error.png
  142. BIN  public/images/exclamation.png
  143. BIN  public/images/head-bg.gif
  144. BIN  public/images/key.png
  145. BIN  public/images/loading.gif
  146. BIN  public/images/minus.gif
  147. BIN  public/images/plus.gif
  148. BIN  public/images/rails.png
  149. BIN  public/images/shadow-trans.png
  150. BIN  public/images/shadow.png
  151. BIN  public/images/tab-bg.jpg
  152. BIN  public/images/tab-tl.gif
  153. BIN  public/images/tab-tr.gif
  154. BIN  public/images/throbber-off.gif
  155. BIN  public/images/throbber-on.gif
  156. BIN  public/images/trash.png
  157. +95 −0 public/javascripts/application.js
  158. +963 −0 public/javascripts/controls.js
  159. +891 −0 public/javascripts/datepicker.js
  160. +973 −0 public/javascripts/dragdrop.js
  161. +1,128 −0 public/javascripts/effects.js
  162. +19 −0 public/javascripts/jquery-1.3.2.min.js
  163. +20 −0 public/javascripts/jquery.tools.min.js
  164. +8 −0 public/javascripts/pagination.js
  165. +4,320 −0 public/javascripts/prototype.js
  166. +5 −0 public/robots.txt
  167. +3 −0  public/stylesheets/print.css
  168. +34 −0 public/stylesheets/reset.css
  169. +302 −0 public/stylesheets/screen.css
  170. +4 −0 script/about
  171. +3 −0  script/console
  172. +3 −0  script/dbconsole
  173. +3 −0  script/destroy
  174. +3 −0  script/generate
  175. +3 −0  script/performance/benchmarker
  176. +3 −0  script/performance/profiler
  177. +3 −0  script/performance/request
  178. +3 −0  script/plugin
  179. +3 −0  script/process/inspector
  180. +3 −0  script/process/reaper
  181. +3 −0  script/process/spawner
  182. +3 −0  script/runner
  183. +3 −0  script/server
  184. +7 −0 test/fixtures/alerts.yml
  185. +7 −0 test/fixtures/attachments.yml
  186. +7 −0 test/fixtures/comments.yml
  187. +19 −0 test/fixtures/contacts.yml
  188. +7 −0 test/fixtures/groups.yml
  189. +9 −0 test/fixtures/priorities.yml
  190. +7 −0 test/fixtures/statuses.yml
  191. +9 −0 test/fixtures/tickets.yml
  192. +25 −0 test/fixtures/users.yml
  193. +8 −0 test/functional/admin_controller_test.rb
  194. +8 −0 test/functional/alerts_controller_test.rb
  195. +8 −0 test/functional/attachments_controller_test.rb
  196. +45 −0 test/functional/comments_controller_test.rb
  197. +45 −0 test/functional/contacts_controller_test.rb
  198. +8 −0 test/functional/dashboard_controller_test.rb
  199. +8 −0 test/functional/password_resets_controller_test.rb
  200. +45 −0 test/functional/tickets_controller_test.rb
  201. +8 −0 test/functional/user_sessions_controller_test.rb
  202. +45 −0 test/functional/users_controller_test.rb
  203. +9 −0 test/performance/browsing_test.rb
  204. +38 −0 test/test_helper.rb
  205. +8 −0 test/unit/alert_test.rb
  206. +8 −0 test/unit/attachment_observer_test.rb
  207. +15 −0 test/unit/attachment_test.rb
  208. +8 −0 test/unit/comment_test.rb
  209. +11 −0 test/unit/contact_test.rb
  210. +10 −0 test/unit/group_test.rb
  211. +4 −0 test/unit/helpers/admin_helper_test.rb
  212. +4 −0 test/unit/helpers/alerts_helper_test.rb
  213. +4 −0 test/unit/helpers/attachments_helper_test.rb
  214. +4 −0 test/unit/helpers/contacts_helper_test.rb
  215. +4 −0 test/unit/helpers/password_resets_helper_test.rb
  216. +8 −0 test/unit/notifier_test.rb
  217. +11 −0 test/unit/priority_test.rb
  218. +10 −0 test/unit/status_test.rb
  219. +8 −0 test/unit/ticket_observer_test.rb
  220. +8 −0 test/unit/ticket_test.rb
  221. +8 −0 test/unit/user_test.rb
  222. +20 −0 vendor/plugins/prawnto/MIT-LICENSE
  223. +12 −0 vendor/plugins/prawnto/README
  224. +22 −0 vendor/plugins/prawnto/Rakefile
  225. +7 −0 vendor/plugins/prawnto/init.rb
  226. +32 −0 vendor/plugins/prawnto/lib/prawnto.rb
  227. +45 −0 vendor/plugins/prawnto/lib/prawnto/action_controller.rb
  228. +12 −0 vendor/plugins/prawnto/lib/prawnto/action_view.rb
  229. +72 −0 vendor/plugins/prawnto/lib/prawnto/template_handler/compile_support.rb
  230. +16 −0 vendor/plugins/prawnto/lib/prawnto/template_handlers/base.rb
  231. +16 −0 vendor/plugins/prawnto/lib/prawnto/template_handlers/dsl.rb
  232. +64 −0 vendor/plugins/prawnto/lib/prawnto/template_handlers/raw.rb
  233. +4 −0 vendor/plugins/prawnto/tasks/prawnto_tasks.rake
  234. +38 −0 vendor/plugins/prawnto/test/action_controller_test.rb
  235. +39 −0 vendor/plugins/prawnto/test/base_template_handler_test.rb
  236. +40 −0 vendor/plugins/prawnto/test/dsl_template_handler_test.rb
  237. +163 −0 vendor/plugins/prawnto/test/raw_template_handler_test.rb
  238. +77 −0 vendor/plugins/prawnto/test/template_handler_test_mocks.rb
9 .gitignore
@@ -0,0 +1,9 @@
+log/*.log
+tmp/*
+tmp/**/*
+doc/api
+doc/app
+db/*.sqlite3
+*.swp
+*~
+.DS_Store
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009 J. Lee Smith
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
70 README.rdoc
@@ -0,0 +1,70 @@
+= TicketMule
+
+No frills, general use support ticket tracking. Easily document and communicate client relations within a support team.
+
+== Features
+
+* Clean interface that is compatible with modern web browsers
+* Add comments and file attachments to tickets
+* Subscribe to ticket updates via email (alerts)
+* Automatically sends an email to the user assigned as owner of a ticket
+* View recent ticket activity and timeline statistics from the dashboard
+* Export ticket in PDF format
+* No complicated permission system...only admins can perform negative actions
+* In-line admin controls to delete comments, attachments, and tickets
+
+== Required Gems
+
+* <em>will_paginate</em>
+* <em>authlogic</em>
+* <em>searchlogic</em>
+* <em>paperclip</em>
+* <em>prawn</em>
+
+To load fake data to test drive TicketMule, you will also need <em>populator</em> and <em>faker</em> gems.
+
+== Install
+
+Basic installation creates a fresh database with a single admin user. See +db/seeds.rb+ for admin user information.
+
+You can choose to test drive TicketMule with fake data as described below.
+
+Create database configuration file and modify as needed:
+
+ $ cp config/database.example.yml config/database.yml
+
+Configure TicketMule settings in the <em>production</em> block of +config/config.yml+. See comments for details.
+
+Modify +config/environment.rb+ settings such as default timezone and rails gem version.
+
+Modify +db/seeds.rb+ to create the seed data for your installation of TicketMule. This is where you create your organization's groups, statuses, and priorities.
+
+Install gems:
+
+ $ rake gems:install
+
+Choose between basic installation or installation with test data:
+
+<b>Basic installation</b> create database and schema, and initialize with seed data:
+
+ $ rake db:setup
+
+<b>Test drive</b> create database and schema, initialize with seed data, and load 150 contacts, 500 tickets, and 4 non-admin users (see +lib/tasks/faker.rake+ for details):
+
+ $ rake faker
+
+== Notes
+
+By default, users can create their own accounts by navigating to /users/new and creating their account. If this is not desired and you only want admins to create user accounts, a small change to +config/routes.rb+ is required. The users resource will need the exception added for the <em>new</em> action. See the comments for the users resource map for details.
+
+When you add an alert to a ticket, you will only receive an email alert when the ticket's attributes change. You will not receive an email alert when a comment or attachment is added.
+
+== License
+
+Copyright (c) 2009 by J. Lee Smith. All rights reserved.
+
+TicketMule is released under the MIT License. See the LICENSE file for details.
+
+== Icons
+
+Fugue icons copyright (c) 2009 by Yusuke Kamiyamane. {Pinvoke.com}[http://www.pinvoke.com]
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
154 app/controllers/admin_controller.rb
@@ -0,0 +1,154 @@
+class AdminController < ApplicationController
+ before_filter :require_admin, :set_current_tab, :get_lists
+
+ def index
+ end
+
+ def add_group
+ @group = Group.new(params[:group])
+ redirect_to('/admin') and return if @group.name.blank?
+
+ respond_to do |format|
+ if @group.save
+ flash[:success] = "Group #{@group.name} was successfully created!"
+ format.html { redirect_to('/admin') }
+ else
+ #set initial tab to display errors...must match tab position in index view
+ @initial_tab_index = 0
+ format.html { render :action => 'index' }
+ end
+ end
+ end
+
+ def add_status
+ @status = Status.new(params[:status])
+ redirect_to('/admin') and return if @status.name.blank?
+
+ respond_to do |format|
+ if @status.save
+ flash[:success] = "Status #{@status.name} was successfully created!"
+ format.html { redirect_to('/admin') }
+ else
+ #set initial tab to display errors...must match tab position in index view
+ @initial_tab_index = 1
+ format.html { render :action => 'index' }
+ end
+ end
+ end
+
+ def add_priority
+ @priority = Priority.new(params[:priority])
+ redirect_to('/admin') and return if @priority.name.blank?
+
+ respond_to do |format|
+ if @priority.save
+ flash[:success] = "Priority #{@priority.name} was successfully created!"
+ format.html { redirect_to('/admin') }
+ else
+ #set initial tab to display errors...must match tab position in index view
+ @initial_tab_index = 2
+ format.html { render :action => 'index' }
+ end
+ end
+ end
+
+ def add_user
+ @user = User.new(params[:user])
+
+ respond_to do |format|
+ if @user.save
+ flash[:success] = "User #{@user.username} was successfully created!"
+ format.html { redirect_to('/admin') }
+ else
+ #set initial tab to display errors...must match tab position in index view
+ @initial_tab_index = 3
+ format.html { render :action => 'index' }
+ end
+ end
+ end
+
+ def toggle_group
+ @group = Group.find(params[:id])
+
+ if @group.enabled?
+ @group.disabled_at = DateTime.now
+ flash_msg = "Group #{@group.name} was successfully disabled!"
+ else
+ @group.disabled_at = nil
+ flash_msg = "Group #{@group.name} was successfully enabled!"
+ end
+
+ respond_to do |format|
+ if @group.save
+ flash[:success] = flash_msg
+ format.html { redirect_to('/admin') }
+ format.xml { render :xml => @group, :status => :created, :location => @group }
+ else
+ format.html { render :action => 'index' }
+ format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def toggle_status
+ @status = Status.find(params[:id])
+
+ if @status.enabled?
+ @status.disabled_at = DateTime.now
+ flash_msg = "Status #{@status.name} was successfully disabled!"
+ else
+ @status.disabled_at = nil
+ flash_msg = "Status #{@status.name} was successfully enabled!"
+ end
+
+ respond_to do |format|
+ if @status.save
+ flash[:success] = flash_msg
+ format.html { redirect_to('/admin') }
+ format.xml { render :xml => @status, :status => :created, :location => @status }
+ else
+ format.html { render :action => 'index' }
+ format.xml { render :xml => @status.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def toggle_priority
+ @priority = Priority.find(params[:id])
+
+ if @priority.enabled?
+ @priority.disabled_at = DateTime.now
+ flash_msg = "Priority #{@priority.name} was successfully disabled!"
+ else
+ @priority.disabled_at = nil
+ flash_msg = "Priority #{@priority.name} was successfully enabled!"
+ end
+
+ respond_to do |format|
+ if @priority.save
+ flash[:success] = flash_msg
+ format.html { redirect_to('/admin') }
+ format.xml { render :xml => @priority, :status => :created, :location => @priority }
+ else
+ format.html { render :action => 'index' }
+ format.xml { render :xml => @priority.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ private
+
+ def set_current_tab
+ @current_tab = :admin
+ end
+
+ def get_lists
+ @groups_enabled = Group.enabled
+ @groups_disabled = Group.disabled
+ @statuses_enabled = Status.enabled
+ @statuses_disabled = Status.disabled
+ @priorities_enabled = Priority.enabled
+ @priorities_disabled = Priority.disabled
+ end
+
+end
32 app/controllers/alerts_controller.rb
@@ -0,0 +1,32 @@
+class AlertsController < ApplicationController
+ before_filter :require_user
+
+ def create
+ @ticket = Ticket.find(params[:id])
+ @alert = @current_user.alerts.build(:ticket_id => @ticket.id)
+
+ respond_to do |format|
+ if @current_user.has_ticket_alert?(@ticket.id) or @alert.save
+ flash[:success] = 'Your alert was added and you will now receive an email any time this ticket is updated!'
+ format.html { redirect_to(@ticket) }
+ format.xml { render :xml => @alert, :status => :created, :location => @alert }
+ else
+ format.html { render 'tickets/show' }
+ format.xml { render :xml => @alert.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ # for the current_user, delete the alert with the incoming ticket id
+ alert = Alert.find_by_ticket_id_and_user_id(params[:id], @current_user.id)
+ alert.destroy
+
+ respond_to do |format|
+ flash[:success] = "Your alert for ticket ##{params[:id]} was removed!"
+ format.html { redirect_to :back }
+ format.xml { head :ok }
+ end
+ end
+
+end
87 app/controllers/application_controller.rb
@@ -0,0 +1,87 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+ helper_method :current_user_session, :current_user
+ before_filter :set_time_zone
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery # :secret => '038c2ea0534ce4156b1aa41d6332e06c'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ filter_parameter_logging :password, :password_confirmation
+
+ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
+ if instance.error_message.kind_of?(Array)
+ %(#{html_tag}<span class="validation-error">&nbsp;#{instance.error_message.join(', ')}</span>)
+ else
+ %(#{html_tag}<span class="validation-error">&nbsp;#{instance.error_message}</span>)
+ end
+ end
+
+ # get the tickets per page user preference...10 by default
+ def tickets_per_page
+ cookies[:tickets_per_page] || '10'
+ end
+
+ def current_user
+ return @current_user if defined?(@current_user)
+ @current_user = current_user_session && current_user_session.record
+ end
+
+ private
+
+ def set_time_zone
+ Time.zone = @current_user.time_zone if @current_user
+ end
+
+ def current_user_session
+ return @current_user_session if defined?(@current_user_session)
+ @current_user_session = UserSession.find
+ end
+
+ def require_user
+ unless current_user
+ store_location
+ flash[:error] = "You must be logged in to access this page!"
+ redirect_to login_path
+ return false
+ end
+ end
+
+ def require_no_user
+ if current_user
+ store_location
+ flash[:error] = "You must be logged out to access this page!"
+ redirect_to root_url
+ return false
+ end
+ end
+
+ def require_admin
+ unless current_user && current_user.admin?
+ store_location
+ flash[:error] = "Unauthorized access!"
+ redirect_to root_url
+ return false
+ end
+ end
+
+ def store_location
+ session[:return_to] =
+ if request.get?
+ request.request_uri
+ else
+ request.referrer
+ end
+ end
+
+ def redirect_back_or_default(default)
+ redirect_to(session[:return_to] || default)
+ session[:return_to] = nil
+ end
+end
54 app/controllers/attachments_controller.rb
@@ -0,0 +1,54 @@
+class AttachmentsController < ApplicationController
+ before_filter :require_user
+ before_filter :set_current_tab
+ before_filter :require_admin, :only => [:destroy]
+
+ def show
+ attachment = Attachment.find(params[:id])
+ Attachment.increment_counter(:download_count, attachment.id)
+
+ #x_sendfile only available on Apache2 w/ mod_xsendfile or Lighttpd
+ if Rails.env.production?
+ send_file attachment.data.path, :type => attachment.content_type, :x_sendfile => true
+ else
+ send_file attachment.data.path, :type => attachment.content_type
+ end
+ end
+
+ def create
+ @ticket = Ticket.find(params[:ticket_id])
+ @attachment = @ticket.attachments.build(params[:attachment])
+ @attachment.user_id = @current_user.id
+
+ redirect_to(@ticket) and return if @attachment.name.blank?
+
+ respond_to do |format|
+ if @attachment.save
+ flash[:success] = 'Your attachment was successfully added!'
+ format.html { redirect_to(@ticket) }
+ format.xml { render :xml => @attachment, :status => :created, :location => @attachment }
+ else
+ format.html { render 'tickets/show' }
+ format.xml { render :xml => @attachment.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ attachment = Attachment.find(params[:id])
+ ticket_id = attachment.ticket_id
+ attachment.destroy
+
+ respond_to do |format|
+ flash[:success] = 'Attachment was successfully deleted!'
+ format.html { redirect_to(ticket_path(ticket_id)) }
+ format.xml { head :ok }
+ end
+ end
+
+ private
+
+ def set_current_tab
+ @current_tab = :tickets
+ end
+end
48 app/controllers/comments_controller.rb
@@ -0,0 +1,48 @@
+class CommentsController < ApplicationController
+ before_filter :require_user
+ before_filter :set_current_tab
+ before_filter :require_admin, :only => [:destroy]
+
+ def create
+ @ticket = Ticket.find(params[:ticket_id])
+ @comment = @ticket.comments.build(params[:comment])
+ @comment.user_id = @current_user.id
+
+ redirect_to(@ticket) and return if @comment.comment.blank?
+
+ if params[:close_ticket]
+ status = Status.find(:first, :conditions => "name = 'Closed'")
+ @ticket.update_attribute(:status_id, status.id)
+ @comment.comment = "<strong>Status</strong> changed to closed<br/>" + @comment.comment
+ end
+
+ respond_to do |format|
+ if @comment.save
+ flash[:success] = 'Your comment was successfully added!'
+ format.html { redirect_to(@ticket) }
+ format.xml { render :xml => @comment, :status => :created, :location => @comment }
+ else
+ format.html { render 'tickets/show' }
+ format.xml { render :xml => @comment.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ comment = Comment.find(params[:id])
+ ticket_id = comment.ticket_id
+ comment.destroy
+
+ respond_to do |format|
+ flash[:success] = 'Comment was successfully deleted!'
+ format.html { redirect_to(ticket_path(ticket_id)) }
+ format.xml { head :ok }
+ end
+ end
+
+ private
+
+ def set_current_tab
+ @current_tab = :tickets
+ end
+end
115 app/controllers/contacts_controller.rb
@@ -0,0 +1,115 @@
+class ContactsController < ApplicationController
+ before_filter :require_user
+ before_filter :lookup_contact, :only => [:show, :edit, :update, :destroy, :toggle]
+ before_filter :set_current_tab
+ before_filter :require_admin, :only => [:destroy, :toggle]
+
+ def index
+ unless params[:index]
+ @contacts = Contact.paginate :page => params[:page], :order => 'last_name, first_name', :per_page => 10
+ else
+ @initial = params[:index]
+ @contacts = Contact.paginate :page => params[:page], :conditions => ["last_name like ?", @initial+'%'], :order => 'last_name, first_name', :per_page => 10
+ end
+ @total_contacts = @contacts.total_entries
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.js # index.js.erb
+ format.xml { render :xml => @contacts }
+ end
+ end
+
+ def show
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @contact }
+ end
+ end
+
+ def new
+ @contact = Contact.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @contact }
+ end
+ end
+
+ def edit
+ end
+
+ def create
+ @contact = Contact.new(params[:contact])
+
+ respond_to do |format|
+ if @contact.save
+ flash[:success] = "#{@contact.full_name} was successfully created!"
+ format.html { redirect_to(@contact) }
+ format.xml { render :xml => @contact, :status => :created, :location => @contact }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @contact.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def update
+ respond_to do |format|
+ if @contact.update_attributes(params[:contact])
+ flash[:success] = "#{@contact.full_name} was successfully updated!"
+ format.html { redirect_to(@contact) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => 'edit' }
+ format.xml { render :xml => @contact.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ @contact.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(contacts_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ def toggle
+ if @contact.enabled?
+ @contact.disabled_at = DateTime.now
+ flash_msg = "#{@contact.full_name} was successfully disabled!"
+ else
+ @contact.disabled_at = nil
+ flash_msg = "#{@contact.full_name} was successfully enabled!"
+ end
+
+ respond_to do |format|
+ if @contact.save
+ flash[:success] = flash_msg
+ format.html { redirect_to(@contact) }
+ format.xml { render :xml => @contact, :status => :created, :location => @contact }
+ else
+ format.html { render :action => 'index' }
+ format.xml { render :xml => @contact.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ private
+
+ def lookup_contact
+ begin
+ @contact = Contact.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ logger.error(":::Attempt to access invalid contact_id => #{params[:id]}")
+ flash[:error] = 'You have requested an invalid contact!'
+ redirect_to contacts_path
+ end
+ end
+
+ def set_current_tab
+ @current_tab = :contacts
+ end
+end
31 app/controllers/dashboard_controller.rb
@@ -0,0 +1,31 @@
+class DashboardController < ApplicationController
+ before_filter :require_user
+ before_filter :set_current_tab
+
+ def index
+ @active_tickets = Ticket.not_closed.active_tickets
+ @closed_tickets = Ticket.closed_tickets
+
+ @timeline = ((Date.parse 30.days.ago.to_s)..(Date.yesterday)).inject([]){ |accum, date| accum << date.to_s }
+ @timeline_opened_tickets = Ticket.timeline_opened_tickets((DateTime.now - 30.days).midnight, (DateTime.now).midnight)
+ @timeline_closed_tickets = Ticket.timeline_closed_tickets((DateTime.now - 30.days).midnight, (DateTime.now).midnight)
+
+ @max_opened = 0
+ @max_closed = 0
+
+ @timeline_opened_tickets.each_value do |v|
+ @max_opened = v unless v <= @max_opened
+ end
+
+ @timeline_closed_tickets.each_value do |v|
+ @max_closed = v unless v <= @max_closed
+ end
+ end
+
+ private
+
+ def set_current_tab
+ @current_tab = :dashboard
+ end
+
+end
50 app/controllers/password_resets_controller.rb
@@ -0,0 +1,50 @@
+class PasswordResetsController < ApplicationController
+ before_filter :load_user_using_perishable_token, :only => [:edit, :update]
+ before_filter :require_no_user
+
+ def new
+ render :layout => 'user_sessions'
+ end
+
+ def create
+ @user = User.find_by_email(params[:email])
+ if @user
+ @user.deliver_password_reset_instructions!
+ flash[:success] = "Instructions to reset your password have been emailed to you. Please check your email."
+ redirect_to login_path
+ else
+ flash[:error] = "No user was found with that email address!"
+ redirect_to new_password_reset_path
+ end
+ end
+
+ def edit
+ render :layout => 'user_sessions'
+ end
+
+ def update
+ if params[:user][:password].empty? and params[:user][:password_confirmation].empty?
+ redirect_to edit_password_reset_path and return
+ end
+ @user.password = params[:user][:password]
+ @user.password_confirmation = params[:user][:password_confirmation]
+ if @user.save
+ flash[:success] = "Password successfully updated!"
+ redirect_to root_path
+ else
+ render :action => :edit, :layout => 'user_sessions'
+ end
+ end
+
+ private
+ def load_user_using_perishable_token
+ @user = User.find_using_perishable_token(params[:id])
+ unless @user
+ flash[:error] = "We're sorry, but we could not locate your account." +
+ "If you are having issues try copying and pasting the URL " +
+ "from your email into your browser or restarting the " +
+ "reset password process."
+ redirect_to login_path
+ end
+ end
+end
144 app/controllers/tickets_controller.rb
@@ -0,0 +1,144 @@
+class TicketsController < ApplicationController
+ before_filter :require_user
+ before_filter :set_current_tab
+ before_filter :lookup_ticket, :only => [:edit, :update, :destroy]
+ before_filter :require_admin, :only => [:destroy]
+ before_filter :get_alert, :only => [:show]
+ cache_sweeper :audit_sweeper
+
+ def index
+ @tickets_per_page = tickets_per_page
+ @search = Ticket.search(params[:search])
+ @closed_status = Status.find(:first, :select => 'id', :conditions => "name = 'Closed'")
+ @open_status = Status.find(:first, :select => 'id', :conditions => "name = 'Open'")
+
+ if params[:search]
+ if !params[:search][:created_at_gte].blank?
+ start_date = Date.strptime(params[:search][:created_at_gte],"%Y-%m-%d")
+ @search.created_at_gte = start_date
+ start_date = start_date.midnight.gmtime
+ params[:search][:created_at_gte] = start_date.midnight
+ end
+ if !params[:search][:created_at_lt].blank?
+ end_date = Date.strptime(params[:search][:created_at_lt],"%Y-%m-%d")
+ @search.created_at_lt = end_date
+ end_date = end_date.next.midnight.gmtime
+ params[:search][:created_at_lt] = end_date.midnight
+ end
+ @tickets = Ticket.search(params[:search]).paginate(
+ :page => params[:page],
+ :include => [:creator, :owner, :group, :status, :priority, :contact],
+ :order => 'updated_at DESC',
+ :per_page => @tickets_per_page)
+ else
+ @tickets = Ticket.not_closed.paginate(
+ :page => params[:page],
+ :include => [:creator, :owner, :group, :status, :priority, :contact],
+ :order => 'updated_at DESC',
+ :per_page => @tickets_per_page)
+ end
+
+ @total_tickets = @tickets.total_entries
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.js # index.js.erb
+ format.xml { render :xml => @tickets }
+ end
+ end
+
+ def show
+ begin
+ @ticket = Ticket.find(params[:id], :include => { :comments => :user, :attachments => :user })
+ rescue ActiveRecord::RecordNotFound
+ logger.error(":::Attempt to access invalid ticket_id => #{params[:id]}")
+ flash[:error] = "You have requested an invalid ticket!"
+ redirect_to tickets_path and return
+ end
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.pdf { render :layout => false }
+ format.xml { render :xml => @ticket }
+ end
+ end
+
+ def new
+ @ticket = Ticket.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @ticket }
+ end
+ end
+
+ def edit
+ end
+
+ def create
+ # scope creation of new ticket to current_user
+ @ticket = @current_user.created_tickets.build(params[:ticket])
+
+ respond_to do |format|
+ if @ticket.save
+ flash[:success] = "Ticket ##{@ticket.id} was successfully created!"
+ format.html { redirect_to(@ticket) }
+ format.xml { render :xml => @ticket, :status => :created, :location => @ticket }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @ticket.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def update
+ respond_to do |format|
+ if @ticket.update_attributes(params[:ticket])
+ flash[:success] = 'Ticket was successfully updated!'
+ format.html { redirect_to(@ticket) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @ticket.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ @ticket.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(tickets_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ # Sets a cookie on the user's browser to indicate the tickets per page value
+ def set_tickets_per_page
+ @per_page = params[:per_page]
+ cookies[:tickets_per_page] = { :value => "#{@per_page}", :expires => 1.year.from_now }
+ flash[:success] = "You are now viewing #{@per_page} tickets per page!"
+ redirect_to tickets_path
+ end
+
+ private
+
+ def lookup_ticket
+ begin
+ @ticket = Ticket.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ logger.error(":::Attempt to access invalid ticket_id => #{params[:id]}")
+ flash[:error] = "You have requested an invalid ticket!"
+ redirect_to tickets_path and return
+ end
+ end
+
+ def set_current_tab
+ @current_tab = :tickets
+ end
+
+ def get_alert
+ @alert = Alert.find_by_user_id_and_ticket_id(@current_user.id, params[:id])
+ end
+
+end
34 app/controllers/user_sessions_controller.rb
@@ -0,0 +1,34 @@
+class UserSessionsController < ApplicationController
+ before_filter :require_no_user, :only => [:new, :create]
+ before_filter :require_user, :only => :destroy
+
+ def new
+ @user_session = UserSession.new
+ end
+
+ def create
+ @user_session = UserSession.new(params[:user_session])
+ if @user_session.save
+ msg = "Welcome back <strong>#{@user_session.user.username}</strong>!"
+ if @user_session.user.last_login_at
+ msg = msg + "&nbsp;Your last login was on <strong>#{@user_session.user.last_login_at.strftime("%Y-%m-%d %I:%M %p")}</strong> from <strong>#{@user_session.user.last_login_ip}</strong>"
+ end
+ flash[:success] = msg
+ redirect_back_or_default dashboard_index_path
+ else
+ if @user_session.being_brute_force_protected?
+ flash[:error] = "User is locked: exceeded failed login limit!"
+ else
+ flash[:error] = "Invalid login attempt!"
+ end
+ redirect_to login_path
+ end
+ end
+
+ def destroy
+ current_user_session.destroy
+ flash[:success] = "Logout successful!"
+ redirect_back_or_default login_path
+ end
+
+end
117 app/controllers/users_controller.rb
@@ -0,0 +1,117 @@
+class UsersController < ApplicationController
+ before_filter :require_no_user, :only => [:new, :create]
+ before_filter :require_user, :only => [:show, :index, :edit, :update]
+ before_filter :require_admin, :only => [:destroy]
+ before_filter :lookup_user, :only => [:show, :destroy, :toggle]
+ before_filter :set_current_tab
+
+ def new
+ @user = User.new
+ render :layout => 'user_sessions'
+ end
+
+ def index
+ unless params[:index]
+ @users = User.paginate :page => params[:page], :order => 'last_name, first_name', :per_page => 10
+ else
+ @initial = params[:index]
+ @users = User.paginate :page => params[:page], :conditions => ["last_name like ?", @initial+'%'], :order => 'last_name, first_name', :per_page => 10
+ end
+ @total_users = @users.total_entries
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.js # index.js.erb
+ format.xml { render :xml => @users }
+ end
+ end
+
+ def create
+ @user = User.new(params[:user])
+ if @user.save
+ flash[:success] = 'Account created successfully!'
+ redirect_back_or_default dashboard_index_path
+ else
+ render :action => :new, :layout => 'user_sessions'
+ end
+ end
+
+ def show
+ @recently_assigned_to = Ticket.recently_assigned_to(@user.id)
+
+ respond_to do |format|
+ format.html #show.html.erb
+ format.xml { render :xml => @user }
+ end
+ end
+
+ def edit
+ if @current_user.admin?
+ @user = User.find(params[:id])
+ else
+ @user = @current_user
+ end
+ end
+
+ def update
+ if @current_user.admin?
+ @user = lookup_user
+ else
+ @user = @current_user
+ end
+
+ if @user.update_attributes(params[:user])
+ flash[:success] = 'Account updated successfully!'
+ redirect_to user_path(@user.id)
+ else
+ render :action => :edit
+ end
+ end
+
+ def destroy
+ @user.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(users_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ def toggle
+ if @user.enabled?
+ @user.disabled_at = DateTime.now
+ flash_msg = "#{@user.username} was successfully disabled!"
+ else
+ @user.disabled_at = nil
+ flash_msg = "#{@user.username} was successfully enabled!"
+ end
+
+ respond_to do |format|
+ if @user.save
+ flash[:success] = flash_msg
+ format.html { redirect_to(@user) }
+ format.xml { render :xml => @user, :status => :created, :location => @user }
+ else
+ format.html { render :action => "index" }
+ format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ private
+
+ def lookup_user
+ begin
+ @user = User.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ logger.error(":::Attempt to access invalid user_id => #{params[:id]}")
+ flash[:error] = 'You have requested an invalid user!'
+ redirect_to users_path
+ end
+ end
+
+ def set_current_tab
+ @current_tab = :users
+ end
+
+end
2  app/helpers/admin_helper.rb
@@ -0,0 +1,2 @@
+module AdminHelper
+end
2  app/helpers/alerts_helper.rb
@@ -0,0 +1,2 @@
+module AlertsHelper
+end
25 app/helpers/application_helper.rb
@@ -0,0 +1,25 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+
+ def nice_date date
+ # 31 Jan 2009 12:00 pm
+ h date.strftime("%d %b %Y %I:%M %p")
+ end
+
+ def listing_date date
+ # 2009-01-31 12:00 pm
+ h date.strftime("%Y-%m-%d %I:%M %p")
+ end
+
+ # Show flash messages
+ def show_flash
+ [:success, :error, :warning].collect do |key|
+ content_tag(:div, content_tag(:p, flash[key]), :class => "flash f-#{key}") unless flash[key].blank?
+ end.join
+ end
+
+ # Generate application tabs, signifying current tab
+ def tab_for(tab, link, label=nil)
+ content_tag(:li, link_to(content_tag(:span, label || tab.to_s.titleize), link, :class => ("active" if @current_tab == tab)))
+ end
+end
2  app/helpers/attachments_helper.rb
@@ -0,0 +1,2 @@
+module AttachmentsHelper
+end
2  app/helpers/comments_helper.rb
@@ -0,0 +1,2 @@
+module CommentsHelper
+end
2  app/helpers/contacts_helper.rb
@@ -0,0 +1,2 @@
+module ContactsHelper
+end
2  app/helpers/dashboard_helper.rb
@@ -0,0 +1,2 @@
+module DashboardHelper
+end
2  app/helpers/password_resets_helper.rb
@@ -0,0 +1,2 @@
+module PasswordResetsHelper
+end
57 app/helpers/tickets_helper.rb
@@ -0,0 +1,57 @@
+module TicketsHelper
+
+ # view helper for building ticket form
+ def setup_ticket(ticket)
+ # fill select boxes with associated models
+ @contact_select = Contact.enabled(:select => "id, first_name, last_name")
+ @group_select = Group.enabled(:select => "id, name")
+ @status_select = Status.enabled(:select => "id, name")
+ @priority_select = Priority.enabled(:select => "id, name")
+ @owner_select = User.enabled(:select => "id, username")
+
+ unless ticket.id.blank?
+ unless ticket.contact.blank? || ticket.contact.enabled?
+ disabled_contact = Contact.find(ticket.contact.id)
+ @contact_select.unshift(disabled_contact)
+ end
+ unless ticket.group.blank? || ticket.group.enabled?
+ disabled_group = Group.find(ticket.group_id)
+ @group_select.unshift(disabled_group)
+ end
+ unless ticket.status.blank? || ticket.status.enabled?
+ disabled_status = Status.find(ticket.status_id)
+ @status_select.unshift(disabled_status)
+ end
+ unless ticket.priority.blank? || ticket.priority.enabled?
+ disabled_priority = Priority.find(ticket.priority_id)
+ @priority_select.unshift(disabled_priority)
+ end
+ unless ticket.owner.blank? || ticket.owner.enabled?
+ disabled_owner = User.find(ticket.owned_by)
+ @owner_select.unshift(disabled_owner)
+ end
+ end
+
+ # build attachment file_field and return ticket
+ returning(ticket) do |t|
+ t.attachments.build #if t.attachments.empty?
+ end
+ end
+
+ def ticket_filter_links(status_name, user_id=nil)
+ if user_id.nil?
+ if status_name.downcase == 'closed'
+ content_tag(:li, link_to("All Closed Tickets", tickets_path + "?search[status_id_equals]=#{@closed_status.id}"))
+ else
+ content_tag(:li, link_to("All Active Tickets", tickets_path))
+ end
+ else
+ if status_name.downcase == 'closed'
+ content_tag(:li, link_to("My Closed Tickets", tickets_path + "?search[status_id_equals]=#{@closed_status.id}&search[owned_by_equals]=#{user_id}"))
+ else
+ content_tag(:li, link_to("My Open Tickets", tickets_path + "?search[status_id_equals]=#{@open_status.id}&search[owned_by_equals]=#{user_id}"))
+ end
+ end
+ end
+
+end
2  app/helpers/user_sessions_helper.rb
@@ -0,0 +1,2 @@
+module UserSessionsHelper
+end
2  app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
10 app/models/alert.rb
@@ -0,0 +1,10 @@
+class Alert < ActiveRecord::Base
+
+ # Default Order
+ default_scope :order => 'created_at DESC'
+
+ # Associations
+ belongs_to :ticket
+ belongs_to :user
+
+end
64 app/models/attachment.rb
@@ -0,0 +1,64 @@
+class Attachment < ActiveRecord::Base
+
+ # Paperclip config
+ has_attached_file :data,
+ :url => "/attachments/:ticket_id/:id",
+ :path => ":rails_root/attachments/:ticket_id/:id/:basename.:extension"
+
+ # Associations
+ belongs_to :ticket, :touch => true
+ belongs_to :user
+
+ # Validations
+ validates_attachment_presence :data
+ #validates_attachment_size :data, :less_than => 10.megabytes, :message => "attachment file size is limited to 10 megabytes."
+ validates_attachment_size :data, :less_than => APP_CONFIG['attachment_size_limit'].megabytes, :message => "attachment file size is limited to 10 megabytes."
+ validates_presence_of :ticket_id, :user_id
+
+ attr_protected :data_file_name, :data_content_type, :data_file_size
+
+ def url
+ data.url
+ end
+
+ def name
+ data_file_name
+ end
+
+ def file_size
+ data_file_size
+ end
+
+ def nice_file_size
+ file_size < 1.kilobyte ? "(#{file_size}B)" : "(#{file_size/1.kilobyte}KB)"
+ end
+
+ def content_type
+ data_content_type
+ end
+
+ def content_type_class
+ if self.content_type.index('image') != nil and self.content_type.index('image') >= 0 then
+ 'image'
+ elsif self.content_type.index('video') != nil and self.content_type.index('video') >= 0 then
+ 'video'
+ elsif self.content_type.index('audio') != nil and self.content_type.index('audio') >= 0 then
+ 'audio'
+ elsif self.content_type.index('pdf') != nil and self.content_type.index('pdf') >= 0 then
+ 'pdf'
+ elsif self.content_type.index('text') != nil and self.content_type.index('text') >= 0 then
+ 'text'
+ elsif self.content_type.index('msword') != nil and self.content_type.index('msword') >= 0 then
+ 'word'
+ elsif self.content_type.index('excel') != nil and self.content_type.index('excel') >= 0 then
+ 'excel'
+ elsif self.content_type.index('powerpoint') != nil and self.content_type.index('powerpoint') >= 0 then
+ 'ppt'
+ elsif self.content_type.index('zip') != nil and self.content_type.index('zip') >= 0 then
+ 'zip'
+ else
+ 'unknown'
+ end
+ end
+
+end
14 app/models/attachment_observer.rb
@@ -0,0 +1,14 @@
+class AttachmentObserver < ActiveRecord::Observer
+
+ def after_create(attachment)
+ ticket = attachment.ticket
+ ticket.comments.create(:user_id => attachment.user.id) do |c|
+ c.comment = "<strong>Attached</strong> #{attachment.name} #{attachment.nice_file_size}"
+ end
+ end
+
+ def after_destroy(attachment)
+ attachment.logger.info "::::::::::::::Deleted #{attachment.name}"
+ end
+
+end
62 app/models/audit_sweeper.rb
@@ -0,0 +1,62 @@
+class AuditSweeper < ActionController::Caching::Sweeper
+ observe Ticket
+
+ def after_validation_on_update(ticket)
+ if ticket.errors.blank? # do nothing if the ticket has errors
+ # if updated_at is the only attribute changed (ticket was touched), skip audit comment
+ unless ticket.only_touched?
+ user = controller.current_user
+ text = ""
+
+ if ticket.title_changed?
+ text = text + "<strong>Title</strong> changed to #{ticket.title}"
+ end
+ if ticket.contact_id_changed?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Contact</strong> changed to #{ticket.contact.full_name}"
+ end
+ if ticket.group_id_changed?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Group</strong> changed to #{ticket.group.name}"
+ end
+ if ticket.status_id_changed?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Status</strong> changed to #{ticket.status.name}"
+ end
+ if ticket.owned_by_changed?
+ if ticket.owned_by.nil?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Owner</strong> removed"
+ else
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Owner</strong> changed to #{ticket.owner.username}"
+ end
+ end
+ if ticket.priority_id_changed?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Priority</strong> changed to #{ticket.priority.name}"
+ end
+ if ticket.details_changed?
+ text = text + "<br/>" if !text.empty?
+ text = text + "<strong>Details</strong> changed"
+ end
+
+ ticket.comments.create(:user_id => user.id) do |c|
+ c.comment = text
+ end
+
+ unless ticket.alert_users.blank?
+ user_list = Array.new
+ ticket.alert_users.each do |u|
+ # don't send the email to the person making the update
+ unless u.id == user.id
+ user_list.push(u.email)
+ end
+ end
+ Notifier.deliver_ticket_alert(ticket,user_list,text)
+ end
+ end
+ end
+ end
+
+end
10 app/models/comment.rb
@@ -0,0 +1,10 @@
+class Comment < ActiveRecord::Base
+
+ # Associations
+ belongs_to :ticket, :counter_cache => true, :touch => true
+ belongs_to :user
+
+ # Validations
+ validates_presence_of :comment
+
+end
24 app/models/contact.rb
@@ -0,0 +1,24 @@
+class Contact < ActiveRecord::Base
+
+ # Associations
+ has_many :tickets
+
+ # Scopes
+ named_scope :enabled, :order => 'last_name, first_name', :conditions => { :disabled_at => nil }
+
+ # Validations
+ validates_presence_of :last_name, :email
+
+ def full_name
+ if first_name.blank?
+ last_name
+ else
+ [last_name, first_name].compact.join(', ')
+ end
+ end
+
+ def enabled?
+ disabled_at.blank?
+ end
+
+end
18 app/models/group.rb
@@ -0,0 +1,18 @@
+class Group < ActiveRecord::Base
+
+ # Associations
+ has_many :tickets
+
+ # Scopes
+ named_scope :enabled, :order => 'name', :conditions => { :disabled_at => nil }
+ named_scope :disabled, :order => 'name', :conditions => ['disabled_at IS NOT NULL']
+
+ # Validations
+ validates_presence_of :name
+ validates_uniqueness_of :name, :case_sensitive => false
+
+ def enabled?
+ disabled_at.blank?
+ end
+
+end
27 app/models/notifier.rb
@@ -0,0 +1,27 @@
+class Notifier < ActionMailer::Base
+ default_url_options[:host] = APP_CONFIG['domain_name']
+
+ def password_reset_instructions(user)
+ subject "Password Reset Instructions"
+ from APP_CONFIG['noreply_email']
+ recipients user.email
+ sent_on Time.now
+ body :edit_password_reset_url => edit_password_reset_url(user.perishable_token)
+ end
+
+ def ticket_alert(ticket, users, comment)
+ subject "Ticket ##{ticket.id} was updated..."
+ from APP_CONFIG['noreply_email']
+ bcc users
+ sent_on Time.now
+ body :ticket => ticket, :audit_comment => comment
+ end
+
+ def owner_alert(ticket,owner_email)
+ subject "Ticket ##{ticket.id} was assigned to you..."
+ from APP_CONFIG['noreply_email']
+ recipients owner_email
+ sent_on Time.now
+ body :ticket => ticket
+ end
+end
23 app/models/priority.rb
@@ -0,0 +1,23 @@
+class Priority < ActiveRecord::Base
+
+ # Associations
+ has_many :tickets
+
+ # Scopes
+ named_scope :enabled, :order => 'name', :conditions => { :disabled_at => nil }
+ named_scope :disabled, :order => 'name', :conditions => ['disabled_at IS NOT NULL']
+
+ # Validations
+ validates_presence_of :name
+ validates_uniqueness_of :name, :case_sensitive => false
+
+ # for css purposes, distinguish between standard or custom priority
+ def standard?
+ self.name.downcase == "high" || self.name.downcase == "medium" || self.name.downcase == "low"
+ end
+
+ def enabled?
+ disabled_at.blank?
+ end
+
+end
18 app/models/status.rb
@@ -0,0 +1,18 @@
+class Status < ActiveRecord::Base
+
+ # Associations
+ has_many :tickets
+
+ # Scopes
+ named_scope :enabled, :order => 'name', :conditions => { :disabled_at => nil }
+ named_scope :disabled, :order => 'name', :conditions => ['disabled_at IS NOT NULL']
+
+ # Validations
+ validates_presence_of :name
+ validates_uniqueness_of :name, :case_sensitive => false
+
+ def enabled?
+ disabled_at.blank?
+ end
+
+end
55 app/models/ticket.rb
@@ -0,0 +1,55 @@
+class Ticket < ActiveRecord::Base
+
+ # Associations
+ belongs_to :group
+ belongs_to :status
+ belongs_to :priority
+ belongs_to :creator, :class_name => "User", :foreign_key => "created_by"
+ belongs_to :owner, :class_name => "User", :foreign_key => "owned_by"
+ belongs_to :contact
+ has_many :comments, :dependent => :destroy
+ has_many :attachments, :dependent => :destroy, :class_name => '::Attachment'
+ has_many :alerts, :dependent => :destroy
+ has_many :alert_users, :through => :alerts, :class_name => 'User', :source => :user
+
+ # Validations
+ validates_presence_of :title, :group_id, :status_id, :priority_id, :contact_id
+
+ # Callbacks
+ before_update :set_closed_at
+
+ # Scopes
+ named_scope :not_closed, :joins => :status, :conditions => ['statuses.name <> ?', 'Closed']
+ named_scope :recently_assigned_to, lambda { | user_id | { :limit => 5, :conditions => { :owned_by => user_id }, :include => [:creator, :owner, :group, :status, :priority, :contact], :order => ['updated_at DESC']} }
+ named_scope :active_tickets, :limit => 5, :include => [:creator, :owner, :group, :status, :priority], :order => ['updated_at DESC']
+ named_scope :closed_tickets, :limit => 5, :joins => :status, :include => [:creator, :owner, :group, :status, :priority], :conditions => ['statuses.name = ?', 'Closed'], :order => ['closed_at DESC']
+
+ def self.timeline_opened_tickets(from_date, to_date)
+ self.count(:group => 'date(created_at)', :having => ['created_at >= ? and created_at < ?', from_date, to_date], :order => 'created_at')
+ end
+
+ def self.timeline_closed_tickets(from_date, to_date)
+ self.count(:group => 'date(closed_at)', :having => ['closed_at >= ? and closed_at < ?', from_date, to_date], :order => 'closed_at')
+ end
+
+ def closed?
+ status.name == 'Closed'
+ end
+
+ def only_touched?
+ self.changed.size == 1 and self.changed[0] == "updated_at"
+ end
+
+ protected
+
+ def set_closed_at
+ # update the closed_at timestamp if the ticket is being closed
+ if closed?
+ logger.info("Ticket is being closed!")
+ self.closed_at = DateTime.now if self.closed_at.nil?
+ else
+ self.closed_at = nil unless self.closed_at.nil?
+ end
+ end
+
+end
11 app/models/ticket_observer.rb
@@ -0,0 +1,11 @@
+class TicketObserver < ActiveRecord::Observer
+
+ def after_save(ticket)
+ # send alert to the owner of the ticket
+ unless ticket.only_touched?
+ if ticket.owned_by_changed? and !ticket.owned_by.nil?
+ Notifier.deliver_owner_alert(ticket, ticket.owner.email)
+ end
+ end
+ end
+end
53 app/models/user.rb
@@ -0,0 +1,53 @@
+class User < ActiveRecord::Base
+
+ # Authlogic config
+ acts_as_authentic do |c|
+ c.logged_in_timeout = APP_CONFIG['session_timeout'].minutes
+ c.validates_length_of_login_field_options :in => 4..35
+ c.validates_format_of_login_field_options :with => /^[A-Z0-9_]*$/i, :message => "must contain only letters, numbers, and underscores"
+ end
+
+ # Associations
+ has_many :created_tickets, :class_name => "Ticket", :foreign_key => "created_by"
+ has_many :opened_tickets, :class_name => "Ticket", :foreign_key => "opened_by"
+ has_many :comments
+ has_many :attachments
+ has_many :alerts, :dependent => :destroy
+ has_many :alert_tickets, :through => :alerts, :class_name => 'Ticket', :source => :ticket
+
+ # Validations
+ validates_presence_of :first_name, :last_name
+ validates_confirmation_of :email
+
+ # Scopes
+ named_scope :enabled, :order => 'username', :conditions => { :disabled_at => nil }
+
+ attr_protected :admin
+
+ def full_name
+ if first_name.blank?
+ last_name
+ else
+ [last_name, first_name].compact.join(', ')
+ end
+ end
+
+ def has_ticket_alert?(ticket_id)
+ self.alert_tickets.each do |ticket|
+ if ticket_id == ticket.id
+ return true
+ end
+ end
+ return false
+ end
+
+ def deliver_password_reset_instructions!
+ reset_perishable_token!
+ Notifier.deliver_password_reset_instructions(self)
+ end
+
+ def enabled?
+ disabled_at.blank?
+ end
+
+end
12 app/models/user_session.rb
@@ -0,0 +1,12 @@
+class UserSession < Authlogic::Session::Base
+
+ # ban user after failed login limit is exceeded
+ consecutive_failed_logins_limit APP_CONFIG['failed_logins_limit']
+
+ # ban user permanently once failed login limit is exceeded
+ failed_login_ban_for 0
+
+ # logout user on session timeout
+ logout_on_timeout true
+
+end
156 app/views/admin/index.html.erb
@@ -0,0 +1,156 @@
+<% content_for :head do %>
+ <%= javascript_include_tag 'jquery.tools.min.js' -%>
+<% end %>
+
+<h2>Admin</h2>
+
+<%= error_messages_for :group %>
+<%= error_messages_for :status %>
+<%= error_messages_for :priority %>
+<%= error_messages_for :user %>
+
+<ul class="jquery-tabs">
+ <li><a href="#">Groups</a></li>
+ <li><a href="#">Statuses</a></li>
+ <li><a href="#">Priorities</a></li>
+ <li><a href="#">Users</a></li>
+</ul>
+
+<div class="jquery-tab-panes">
+ <div class="pane">
+ <h3 class="toggle" id="group-enabled-toggle">Enabled</h3>
+ <% unless @groups_enabled.blank? -%>
+ <ul class="admin-list" id="group-enabled-list">
+ <% @groups_enabled.each do |g| -%>
+ <li>
+ <%= g.name -%>
+ <span><%= link_to 'Disable', { :action=> 'toggle_group', :controller => 'admin', :id => g.id }, :class => 'disable', :method => :post, :confirm => "Really disable #{g.name} group?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <h3 class="toggle" id="group-disabled-toggle">Disabled</h3>
+ <% unless @groups_disabled.blank? -%>
+ <ul class="admin-list" id="group-disabled-list">
+ <% @groups_disabled.each do |g| -%>
+ <li>
+ <%= g.name -%>
+ <span><%= link_to 'Enable', { :action=> 'toggle_group', :controller => 'admin', :id => g.id }, :class => 'enable', :method => :post, :confirm => "Really enable #{g.name} group?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <% form_for Group.new, :url => { :action => 'add_group', :controller => 'admin' }, :html => { :class => 'admin-form' } do |f| -%>
+ <p>
+ <%= f.text_field :name, :class => 'textfield' -%>
+ <%= f.submit "Add Group", :class => 'button' -%>
+ </p>
+ <% end -%>
+ </div>
+ <div class="pane">
+ <h3 class="toggle" id="status-enabled-toggle">Enabled</h3>
+ <% unless @statuses_enabled.blank? -%>
+ <ul class="admin-list" id="status-enabled-list">
+ <% @statuses_enabled.each do |s| -%>
+ <li>
+ <%= s.name -%>
+ <span><%= link_to 'Disable', { :action=> 'toggle_status', :controller => 'admin', :id => s.id }, :class => 'disable', :method => :post, :confirm => "Really disable #{s.name} status?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <h3 class="toggle" id="status-disabled-toggle">Disabled</h3>
+ <% unless @statuses_disabled.blank? -%>
+ <ul class="admin-list" id="status-disabled-list">
+ <% @statuses_disabled.each do |s| -%>
+ <li>
+ <%= s.name -%>
+ <span><%= link_to 'Enable', { :action=> 'toggle_status', :controller => 'admin', :id => s.id }, :class => 'enable', :method => :post, :confirm => "Really enable #{s.name} status?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <% form_for Status.new, :url => { :action => 'add_status', :controller => 'admin' }, :html => { :class => 'admin-form' } do |f| -%>
+ <p>
+ <%= f.text_field :name, :class => 'textfield' -%>
+ <%= f.submit "Add Status", :class => 'button' -%>
+ </p>
+ <% end -%>
+ </div>
+ <div class="pane">
+ <h3 class="toggle" id="priority-enabled-toggle">Enabled</h3>
+ <% unless @priorities_enabled.blank? -%>
+ <ul class="admin-list" id="priority-enabled-list">
+ <% @priorities_enabled.each do |p| -%>
+ <li>
+ <%= p.name -%>
+ <span><%= link_to 'Disable', { :action=> 'toggle_priority', :controller => 'admin', :id => p.id }, :class => 'disable', :method => :post, :confirm => "Really disable #{p.name} priority?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <h3 class="toggle" id="priority-disabled-toggle">Disabled</h3>
+ <% unless @priorities_disabled.blank? -%>
+ <ul class="admin-list" id="priority-disabled-list">
+ <% @priorities_disabled.each do |p| -%>
+ <li>
+ <%= p.name -%>
+ <span><%= link_to 'Enable', { :action=> 'toggle_priority', :controller => 'admin', :id => p.id }, :class => 'enable', :method => :post, :confirm => "Really enable #{p.name} priority?" -%></span>
+ </li>
+ <% end -%>
+ </ul>
+ <% end -%>
+ <% form_for Priority.new, :url => { :action => 'add_priority', :controller => 'admin' }, :html => { :class => 'admin-form' } do |f| -%>
+ <p>
+ <%= f.text_field :name, :class => 'textfield' -%>
+ <%= f.submit "Add Priority", :class => 'button' -%>
+ </p>
+ <% end -%>
+ </div>
+ <div class="pane">
+ <% form_for :user, :url => { :action => 'add_user', :controller => 'admin' }, :html => { :id => 'add-user', :class => 'admin-form' } do |f| -%>
+ <p>
+ <label for="user_username">Username</label>
+ <%= f.text_field :username, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_first_name">First name</label>
+ <%= f.text_field :first_name, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_last_name">Last name</label>
+ <%= f.text_field :last_name, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_time_zone">Time zone</label>
+ <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones, { :default => 'Central Time (US & Canada)' }, { :class => 'selectbox' } -%>
+ </p>
+ <p>
+ <label for="user_email">Email</label>
+ <%= f.text_field :email, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_email_confirmation">Confirm Email</label>
+ <%= f.text_field :email_confirmation, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_password">Password</label>
+ <%= f.password_field :password, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <label for="user_password_confirmation">Confirm Password</label>
+ <%= f.password_field :password_confirmation, :size => 30, :class => 'textfield' -%>
+ </p>
+ <p>
+ <%= f.submit 'Add User', :class => 'button' -%>
+ </p>
+ <% end -%>
+ </div>
+</div>
+
+<!-- activate tabs with JavaScript -->
+<script type="text/javascript">
+$(function() {
+ $("ul.jquery-tabs:first").tabs("div.jquery-tab-panes:first > div", { effect: 'fade', fadeOutSpeed: 0 <%= ", initialIndex: #{@initial_tab_index} " unless @initial_tab_index.blank? %>});
+});
+</script>
8 app/views/attachments/_attachment.html.erb
@@ -0,0 +1,8 @@
+<%- unless attachment.id.nil? -%>
+<li>
+ <%= link_to truncate(attachment.name, :length => 60), ticket_attachment_path(ticket.id, attachment.id), :class => attachment.content_type_class -%> <%=attachment.nice_file_size -%> - <em><strong><%= attachment.user.username -%></strong> at <%= nice_date attachment.created_at -%></em>
+ <% if @current_user.admin? -%>
+ <span><%= link_to 'Delete', ticket_attachment_path(ticket.id, attachment.id), :confirm => "Really delete #{attachment.name}?", :method => :delete -%></span>
+ <% end -%>
+</li>
+<%- end -%>
11 app/views/comments/_comment.html.erb
@@ -0,0 +1,11 @@
+<div class="comment">
+ <h4>
+ <span class="counter"><%=comment_counter+1-%></span>
+ <%= link_to comment.user.username, user_path(comment.user.id) unless comment.user.nil? -%>
+ <span class="timestamp">at <%= nice_date comment.created_at -%></span>
+ <% if @current_user.admin? -%>
+ <span class="delete-comment"><%= link_to 'Delete', ticket_comment_path(ticket.id, comment.id), :confirm => "Really delete comment ##{comment_counter+1}?", :method => :delete -%></span>
+ <% end -%>
+ </h4>
+ <%= simple_format(comment.comment) -%>
+</div>
10 app/views/contacts/_alphabet.html.erb
@@ -0,0 +1,10 @@
+<div class="alpha-index">
+ <%= link_to "*", { :action => "index" }, :title => "All" -%>
+ <% ("A".."Z").each do |letter| -%>
+ <% if letter == @initial -%>
+ <%= link_to letter, { :action => "index", :index => letter }, :class => "selected", :title => letter -%>
+ <% else -%>
+ <%= link_to letter, { :action => "index", :index => letter }, :title => letter -%>
+ <% end -%>
+ <% end -%>
+</div>
19 app/views/contacts/_contacts.html.erb
@@ -0,0 +1,19 @@
+<table class="listing" cellspacing="0">
+ <tr class="header">
+ <th>Name</th>
+ <th>Affiliation</th>
+ <th>Disabled at</th>
+ </tr>
+ <% @contacts.each do |contact| -%>
+ <tr class="<%= cycle("list-line-odd", "list-line-even") -%>">
+ <td><%= link_to h(contact.full_name), contact -%></td>
+ <td><%=h contact.affiliation -%></td>
+ <td><%= nice_date contact.disabled_at unless contact.enabled? -%></td>
+ </tr>
+ <% end -%>
+</table>
+
+<%= will_paginate @contacts, :previous_label => '&#8249;', :next_label => '&#8250;' %><span class="loading"><img src="/images/loading.gif" alt="Loading..." /></span>
+<% if @total_contacts -%>
+ <div style="margin: 6px 0; font-style: italic; color: #333; text-align:center;"><%=@total_contacts %> <%= @total_contacts == 1 ? "contact" : "contacts" %> found.</div>
+<% end -%>
20 app/views/contacts/_form.html.erb
@@ -0,0 +1,20 @@
+<dt><label for="contact_first_name">First name</label></dt>
+<dd><%= text_field :contact, :first_name, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_last_name">Last name</label></dt>
+<dd><%= text_field :contact, :last_name, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_email">Email</label></dt>
+<dd><%= text_field :contact, :email, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_mobile_phone">Mobile phone</label></dt>
+<dd><%= text_field :contact, :mobile_phone, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_office_phone">Office phone</label></dt>
+<dd><%= text_field :contact, :office_phone, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_affiliation">Affiliation</label></dt>
+<dd><%= text_field :contact, :affiliation, :class => 'textfield' -%></dd>
+
+<dt><label for="contact_notes">Notes</label></dt>
+<dd><%= text_area :contact, :notes, :size => '70x6', :class => 'textarea' -%></dd>
9 app/views/contacts/edit.html.erb
@@ -0,0 +1,9 @@
+<h2>Editing <%= @contact.full_name -%></h2>
+
+<% form_for @contact do |f| %>
+ <%= f.error_messages %>
+ <dl>
+ <%= render :partial => 'form' -%>
+ <dd><%= f.submit "Update", :class => "button" -%>&nbsp;&nbsp;<%= link_to 'Cancel', @contact %></dd>
+ </dl>
+<% end -%>
18 app/views/contacts/index.html.erb
@@ -0,0 +1,18 @@
+<% content_for :head do -%>
+<%= javascript_include_tag 'pagination' %>
+<% end -%>
+
+<% content_for :button_list do -%>
+<div class="right-container">
+ <%= link_to 'New ticket', new_ticket_path, :method => :get, :class => "big-button" %>
+ <%= link_to 'New contact', new_contact_path, :method => :get, :class => "big-button" %>
+</div>
+<% end -%>
+
+<h2>Contacts</h2>
+
+<%= render :partial => 'alphabet' -%>
+
+<div id="contacts">
+ <%= render 'contacts' -%>
+</div>
1  app/views/contacts/index.js.erb
@@ -0,0 +1 @@
+$("#contacts").html("<%= escape_javascript(render("contacts")) %>");
9 app/views/contacts/new.html.erb
@@ -0,0 +1,9 @@
+<h2>New contact</h2>
+
+<% form_for(@contact) do |f| %>
+ <%= f.error_messages %>
+ <dl>
+ <%= render :partial => 'form' -%>
+ <dd><%= f.submit "Create", :class => "button" -%>&nbsp;&nbsp;<%= link_to 'Cancel', contacts_path -%></dd>
+ </dl>
+<% end -%>
34 app/views/contacts/show.html.erb
@@ -0,0 +1,34 @@
+<div class="right" id="controls">
+ <%= link_to 'Edit', edit_contact_path, :class => "edit-contact" -%> |
+ <% if @current_user.admin? -%>
+ <% unless @contact.enabled? -%>
+ <%= link_to 'Enable', toggle_contact_path, :confirm => "Really enable contact #{@contact.full_name}?", :method => :post, :class => 'enable' -%> |
+ <% else -%>
+ <%= link_to 'Disable', toggle_contact_path, :confirm => "Really disable contact #{@contact.full_name}?", :method => :post, :class => 'disable' -%> |
+ <% end -%>
+ <% end -%>
+ <%= link_to 'Back', contacts_path, :class => "back" -%>
+</div>
+
+<h2<%= @contact.enabled? ? '' : ' class="disabled"' -%>><%=h @contact.full_name -%></h2>
+
+<table class="profile <%= @contact.enabled? ? '' : 'disabled' -%>">
+ <tr>
+ <td class="field">First name:</td><td class="value"><%=h @contact.first_name -%></td>
+ <td class="field">Last name:</td><td class="value"><%=h @contact.last_name -%></td>
+ </tr>
+ <tr>
+ <td class="field">Email:</td><td class="value"><%=h @contact.email -%></td>
+ <td class="field">Affiliation:</td><td class="value"><%=h @contact.affiliation -%></td>
+ </tr>
+ <tr>
+ <td class="field">Mobile phone:</td><td class="value"><%= h @contact.mobile_phone -%></td>
+ <td class="field">Office phone:</td><td class="value"><%=h @contact.office_phone -%></td>
+ </tr>
+ <tr>
+ <td class="field">Updated at:</td>
+ <td class="value" colspan="3"><%= nice_date @contact.updated_at -%></td>
+ </tr>
+ <tr><td class="field-row" colspan="4">Notes:</td></tr>
+ <tr><td class="value-row" colspan="4"><%= simple_format(@contact.notes) -%></td></tr>
+</table>
102 app/views/dashboard/index.html.erb
@@ -0,0 +1,102 @@
+<% content_for :button_list do %>
+<div class="right-container">
+ <%= link_to 'New ticket', new_ticket_path, :method => :get, :class => "big-button" %>
+ <%= link_to 'New contact', new_contact_path, :method => :get, :class => "big-button" %>
+</div>
+<% end %>
+
+<h2>Dashboard</h2>
+
+<h3 class="toggle" id="active-tickets">Active Tickets</h3>
+<div id="active-listing">
+ <table class="listing" cellspacing="0">
+ <thead>
+ <tr class="header">
+ <th>Ticket #</th>
+ <th>Title</th>
+ <th>Group</th>
+ <th>Status</th>
+ <th>Owner</th>
+ <th>Last Activity</th>
+ </tr>
+ </thead>
+ <tbody>
+ <% @active_tickets.each do |ticket| %>
+ <tr class="<%= cycle("list-line-odd", "list-line-even", :name => "active") %>">
+ <% if ticket.priority.standard? -%>
+ <td><%= link_to ticket.id, ticket, { :title => "Ticket ##{ticket.id}: #{ticket.priority.name} priority", :class => "#{ticket.priority.name.downcase}" } -%></td>
+ <% else -%>
+ <td><%= link_to ticket.id, ticket, { :title => "Ticket ##{ticket.id}: #{ticket.priority.name} priority", :class => "custom" } -%></td>
+ <% end -%>
+ <td><%=h truncate(ticket.title, :length => 40) %></td>
+ <td><%= ticket.group.name %></td>
+ <td><%= ticket.status.name %></td>
+ <td><%= link_to ticket.owner.username, user_path(ticket.owner.id) unless ticket.owner.nil? %></td>
+ <td><%= time_ago_in_words ticket.updated_at %> ago</td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+</div>
+
+<h3 class="toggle" id="closed-tickets">Recently Closed Tickets</h3>
+<div id="closed-listing">
+ <table class="listing" cellspacing="0">
+ <thead>
+ <tr class="header">
+ <th>Ticket #</th>
+ <th>Title</th>
+ <th>Group</th>
+ <th>Status</th>
+ <th>Owner</th>
+ <th>Closed At</th>
+ </tr>
+ </thead>
+ <tbody>
+ <% @closed_tickets.each do |ticket| %>
+ <tr class="<%= cycle("list-line-odd", "list-line-even", :name => "closed") %>">
+ <% if ticket.priority.standard? -%>
+ <td><%= link_to ticket.id, ticket, { :title => "Ticket ##{ticket.id}: #{ticket.priority.name} priority", :class => "#{ticket.priority.name.downcase}" } -%></td>
+ <% else -%>
+ <td><%= link_to ticket.id, ticket, { :title => "Ticket ##{ticket.id}: #{ticket.priority.name} priority", :class => "custom" } -%></td>
+ <% end -%>
+ <td><%=h truncate(ticket.title, :length => 40) %></td>
+ <td><%= ticket.group.name %></td>
+ <td><%= ticket.status.name %></td>