Index: test/unit/issue_subcategory_test.rb
===================================================================
--- test/unit/issue_subcategory_test.rb	(revision 0)
+++ test/unit/issue_subcategory_test.rb	(revision 0)
@@ -0,0 +1,24 @@
+require 'test_helper'
+
+class IssueCategorySubcategoryTest < ActiveSupport::TestCase
+  fixtures :issue_subcategories, :issues
+
+  def setup
+    @subcategory = IssueSubcategory.find(1)
+  end
+
+  def test_destroy
+    issue = @subcategory.issues.first
+    @subcategory.destroy
+    # Make sure the category was nullified on the issue
+    assert_nil issue.reload.subcategory
+  end
+
+  def test_destroy_with_reassign
+    issue = @subcategory.issues.first
+    reassign_to = IssueSubcategory.find(2)
+    @subcategory.destroy(reassign_to)
+    # Make sure the issue was reassigned
+    assert_equal reassign_to, issue.reload.subcategory
+  end
+end
Index: test/fixtures/issue_subcategories.yml
===================================================================
--- test/fixtures/issue_subcategories.yml	(revision 0)
+++ test/fixtures/issue_subcategories.yml	(revision 0)
@@ -0,0 +1,9 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+one:
+  issue_category_id: 1
+  name: Toner
+
+two:
+  issue_category_id: 1
+  name: Paper
Index: app/helpers/issue_categories_helper.rb
===================================================================
--- app/helpers/issue_categories_helper.rb	(revision 2399)
+++ app/helpers/issue_categories_helper.rb	(working copy)
@@ -16,4 +16,9 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 module IssueCategoriesHelper
+  def add_issue_subcategory_link(name)
+     link_to_function name do |page|
+      page.insert_html :bottom, :issue_subcategories, :partial => "subcategories", :object => IssueSubcategory.new
+    end
+  end
 end
Index: app/helpers/issues_helper.rb
===================================================================
--- app/helpers/issues_helper.rb	(revision 2399)
+++ app/helpers/issues_helper.rb	(working copy)
@@ -80,6 +80,9 @@
       when 'category_id'
         c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
         c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
+      when 'subcategory_id'
+        c = IssueSubcategory.find_by_id(detail.value) and value = c.name if detail.value
+        c = IssueSubcategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
       when 'fixed_version_id'
         v = Version.find_by_id(detail.value) and value = v.name if detail.value
         v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
@@ -150,6 +153,7 @@
                   l(:field_subject),
                   l(:field_assigned_to),
                   l(:field_category),
+                  l(:field_sub_category),
                   l(:field_fixed_version),
                   l(:field_author),
                   l(:field_start_date),
@@ -176,6 +180,7 @@
                   issue.subject,
                   issue.assigned_to,
                   issue.category,
+                  issue.subcategory,
                   issue.fixed_version,
                   issue.author.name,
                   format_date(issue.start_date),
Index: app/models/issue_category.rb
===================================================================
--- app/models/issue_category.rb	(revision 2399)
+++ app/models/issue_category.rb	(working copy)
@@ -18,11 +18,15 @@
 class IssueCategory < ActiveRecord::Base
   belongs_to :project
   belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
+  has_many :subcategories, :class_name => 'IssueSubcategory', :foreign_key => 'category_id', :dependent => :nullify
   has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
   
   validates_presence_of :name
   validates_uniqueness_of :name, :scope => [:project_id]
   validates_length_of :name, :maximum => 30
+
+  # after something changes, we need to save the changes!
+  after_update :save_sub_categories
   
   alias :destroy_without_reassign :destroy
   
@@ -34,10 +38,40 @@
     end
     destroy_without_reassign
   end
-  
+
+  # we get passed a hash of key => value pairs that describe
+  # the new subcategories.  we need to set their ID to this
+  # objects ID
+  def new_issue_subcategories=(subcategory_elements)
+    subcategory_elements.each do |subcat|
+      subcat[:category_id] = :id
+      subcategories.build(subcat)
+    end
+  end
+
+  # update or delete existing address lines
+  def existing_issue_subcategories=(subcategory_elements)
+    subcategories.reject(&:new_record?).each do |line|
+      attributes = subcategory_elements[line.id.to_s]
+      if attributes
+        line.attributes = attributes
+      else
+        subcategories.delete(line)
+      end
+    end
+  end
+
   def <=>(category)
     name <=> category.name
   end
   
   def to_s; name end
+
+  protected
+  #called after a change (add/update/delete) to save to the database
+  def save_sub_categories
+    subcategories.each do |subcategory|
+      subcategory.save(false)
+    end
+  end
 end
Index: app/models/issue.rb
===================================================================
--- app/models/issue.rb	(revision 2399)
+++ app/models/issue.rb	(working copy)
@@ -24,6 +24,7 @@
   belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
   belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
   belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
+  belongs_to :subcategory, :class_name => 'IssueSubcategory', :foreign_key => 'subcategory_id'
 
   has_many :journals, :as => :journalized, :dependent => :destroy
   has_many :time_entries, :dependent => :delete_all
Index: app/models/issue_subcategory.rb
===================================================================
--- app/models/issue_subcategory.rb	(revision 0)
+++ app/models/issue_subcategory.rb	(revision 0)
@@ -0,0 +1,12 @@
+class IssueSubcategory < ActiveRecord::Base
+  belongs_to :issue_category, :foreign_key => 'category_id'
+  validates_presence_of :name
+  validates_uniqueness_of :name, :scope => [:category_id]
+  validates_length_of :name, :maximum => 30
+
+  def <=>(subcategory)
+    name <=> subcategory.name
+  end
+
+  def to_s; name end
+end
Index: app/controllers/issues_controller.rb
===================================================================
--- app/controllers/issues_controller.rb	(revision 2399)
+++ app/controllers/issues_controller.rb	(working copy)
@@ -21,7 +21,7 @@
   before_filter :find_issue, :only => [:show, :edit, :reply]
   before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
   before_filter :find_project, :only => [:new, :update_form, :preview]
-  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
+  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :update_issue_subcategories]
   before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
   accept_key_auth :index, :changes
 
@@ -43,6 +43,14 @@
   helper :timelog
   include Redmine::Export::PDF
 
+  def update_issue_subcategories
+    results = IssueSubcategory.find(:all, :conditions=>{"category_id"=> params[:category_id]})
+    render :update do |page|
+      page.replace_html 'issue_subcategory', :partial => 'subcategories', :object => results
+    end
+
+  end
+
   def index
     retrieve_query
     sort_init 'id', 'desc'
Index: app/views/issue_categories/_subcategories.rhtml
===================================================================
--- app/views/issue_categories/_subcategories.rhtml	(revision 0)
+++ app/views/issue_categories/_subcategories.rhtml	(revision 0)
@@ -0,0 +1,10 @@
+<span class="issue_subcategory">
+<% new_or_existing = subcategories.new_record? ? 'new' : 'existing' %>
+<% prefix = "category[#{new_or_existing}_issue_subcategories][]" %>
+<% fields_for prefix, subcategories do |subcategory_form| -%>
+<p>
+<%= subcategory_form.text_field :name %>
+<%= link_to_function l(:button_delete) , "$(this).up('.issue_subcategory').remove()" %>
+</p>
+<% end %>
+</span>
\ No newline at end of file
Index: app/views/issue_categories/_form.rhtml
===================================================================
--- app/views/issue_categories/_form.rhtml	(revision 2399)
+++ app/views/issue_categories/_form.rhtml	(working copy)
@@ -3,4 +3,12 @@
 <div class="box">
 <p><%= f.text_field :name, :size => 30, :required => true %></p>
 <p><%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
+<p>
+<%= add_issue_subcategory_link l(:label_issue_subcategory_new) %>
+<div id="issue_subcategories">
+<%= render :partial => 'subcategories', :collection => @category.subcategories %>
 </div>
+
+</p>
+
+</div>
\ No newline at end of file
Index: app/views/issues/_subcategories.rhtml
===================================================================
--- app/views/issues/_subcategories.rhtml	(revision 0)
+++ app/views/issues/_subcategories.rhtml	(revision 0)
@@ -0,0 +1 @@
+<%= subcategories.nil? ? "" : collection_select(:issue, :subcategory_id, subcategories, :id, :name,:include_blank=>true) %>
\ No newline at end of file
Index: app/views/issues/_form.rhtml
===================================================================
--- app/views/issues/_form.rhtml	(revision 2399)
+++ app/views/issues/_form.rhtml	(working copy)
@@ -1,8 +1,8 @@
 <% if @issue.new_record? %>
 <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
 <%= observe_field :issue_tracker_id, :url => { :action => :new },
-                                     :update => :content,
-                                     :with => "Form.serialize('issue-form')" %>
+:update => :content,
+:with => "Form.serialize('issue-form')" %>
 <hr />
 <% end %>
 
@@ -26,15 +26,23 @@
 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
 <% unless @project.issue_categories.empty? %>
-<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
-<%= prompt_to_remote(l(:label_issue_category_new),
-                     l(:label_issue_category_new), 'category[name]', 
-                     {:controller => 'projects', :action => 'add_issue_category', :id => @project},
-                     :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
+<p>
+    <%= f.select(:category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), {:include_blank => true}, {:onchange => "#{remote_function(:url  => {:action => "update_issue_subcategories"},
+                               :with => "'category_id='+value")}"} )%>
+<span id="issue_subcategory">
+<% unless @issue.category.nil? %>
+<%=  f.collection_select(:subcategory_id, @issue.category.subcategories, :id, :name,:include_blank=>true) %>
 <% end %>
-<%= content_tag('p', f.select(:fixed_version_id, 
-                              (@project.versions.sort.collect {|v| [v.name, v.id]}),
-                              { :include_blank => true })) unless @project.versions.empty? %>
+</span>
+    <%= prompt_to_remote(l(:label_issue_category_new),
+l(:label_issue_category_new), 'category[name]',
+{:controller => 'projects', :action => 'add_issue_category', :id => @project},
+:class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %>
+</p>
+<% end %>
+<%= content_tag('p', f.select(:fixed_version_id,
+(@project.versions.sort.collect {|v| [v.name, v.id]}),
+{ :include_blank => true })) unless @project.versions.empty? %>
 </div>
 
 <div class="splitcontentright">
Index: app/views/issues/show.rhtml
===================================================================
--- app/views/issues/show.rhtml	(revision 2399)
+++ app/views/issues/show.rhtml	(working copy)
@@ -31,7 +31,8 @@
     <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
 </tr>
 <tr>
-    <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
+    <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %>
+    <%=h @issue.subcategory ? " / "+@issue.subcategory.name : "-" %></td>
     <% if User.current.allowed_to?(:view_time_entries, @project) %>
     <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
     <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>

Index: lang/en.yml
===================================================================
--- lang/en.yml	(revision 2399)
+++ lang/en.yml	(working copy)
@@ -122,6 +122,7 @@
 field_max_length: Maximum length
 field_value: Value
 field_category: Category
+field_subcategory: Subcategory
 field_title: Title
 field_project: Project
 field_issue: Issue
@@ -321,8 +322,11 @@
 label_issue_status_plural: Issue statuses
 label_issue_status_new: New status
 label_issue_category: Issue category
+label_issue_subcategory: Issue subcategory
 label_issue_category_plural: Issue categories
+label_issue_subcategory_plural: Issue subcategories
 label_issue_category_new: New category
+label_issue_subcategory_new: New subcategory
 label_custom_field: Custom field
 label_custom_field_plural: Custom fields
 label_custom_field_new: New custom field
Index: db/migrate/20090125190215_create_issue_category_subcategories.rb
===================================================================
--- db/migrate/20090125190215_create_issue_category_subcategories.rb	(revision 0)
+++ db/migrate/20090125190215_create_issue_category_subcategories.rb	(revision 0)
@@ -0,0 +1,13 @@
+  class CreateIssueCategorySubcategories < ActiveRecord::Migration
+  def self.up
+    create_table :issue_subcategories do |t|
+      t.integer :category_id
+      t.string :name,  :limit => 30, :null => false
+      t.timestamps
+    end
+  end
+
+  def self.down
+    drop_table :issue_subcategories
+  end
+end
Index: db/migrate/20090125191829_add_sub_categories_to_issues.rb
===================================================================
--- db/migrate/20090125191829_add_sub_categories_to_issues.rb	(revision 0)
+++ db/migrate/20090125191829_add_sub_categories_to_issues.rb	(revision 0)
@@ -0,0 +1,9 @@
+class AddSubCategoriesToIssues < ActiveRecord::Migration
+  def self.up
+	add_column :issues, :subcategory_id, :integer
+  end
+
+  def self.down
+	remove_column :issues, :subcategory_id
+  end
+end
