Patch #2696 ยป add_subcategory_to_categories.diff
| test/unit/issue_subcategory_test.rb (revision 0) | ||
|---|---|---|
| 1 | 
    require 'test_helper'  | 
|
| 2 | ||
| 3 | 
    class IssueCategorySubcategoryTest < ActiveSupport::TestCase  | 
|
| 4 | 
    fixtures :issue_subcategories, :issues  | 
|
| 5 | ||
| 6 | 
    def setup  | 
|
| 7 | 
    @subcategory = IssueSubcategory.find(1)  | 
|
| 8 | 
    end  | 
|
| 9 | ||
| 10 | 
    def test_destroy  | 
|
| 11 | 
    issue = @subcategory.issues.first  | 
|
| 12 | 
    @subcategory.destroy  | 
|
| 13 | 
    # Make sure the category was nullified on the issue  | 
|
| 14 | 
    assert_nil issue.reload.subcategory  | 
|
| 15 | 
    end  | 
|
| 16 | ||
| 17 | 
    def test_destroy_with_reassign  | 
|
| 18 | 
    issue = @subcategory.issues.first  | 
|
| 19 | 
    reassign_to = IssueSubcategory.find(2)  | 
|
| 20 | 
    @subcategory.destroy(reassign_to)  | 
|
| 21 | 
    # Make sure the issue was reassigned  | 
|
| 22 | 
    assert_equal reassign_to, issue.reload.subcategory  | 
|
| 23 | 
    end  | 
|
| 24 | 
    end  | 
|
| test/fixtures/issue_subcategories.yml (revision 0) | ||
|---|---|---|
| 1 | 
    # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html  | 
|
| 2 | ||
| 3 | 
    one:  | 
|
| 4 | 
    issue_category_id: 1  | 
|
| 5 | 
    name: Toner  | 
|
| 6 | ||
| 7 | 
    two:  | 
|
| 8 | 
    issue_category_id: 1  | 
|
| 9 | 
    name: Paper  | 
|
| app/helpers/issue_categories_helper.rb (working copy) | ||
|---|---|---|
| 16 | 16 | 
    # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  | 
| 17 | 17 | |
| 18 | 18 | 
    module IssueCategoriesHelper  | 
| 19 | 
    def add_issue_subcategory_link(name)  | 
|
| 20 | 
    link_to_function name do |page|  | 
|
| 21 | 
    page.insert_html :bottom, :issue_subcategories, :partial => "subcategories", :object => IssueSubcategory.new  | 
|
| 22 | 
    end  | 
|
| 23 | 
    end  | 
|
| 19 | 24 | 
    end  | 
| app/helpers/issues_helper.rb (working copy) | ||
|---|---|---|
| 80 | 80 | 
    when 'category_id'  | 
| 81 | 81 | 
    c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value  | 
| 82 | 82 | 
    c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value  | 
| 83 | 
    when 'subcategory_id'  | 
|
| 84 | 
    c = IssueSubcategory.find_by_id(detail.value) and value = c.name if detail.value  | 
|
| 85 | 
    c = IssueSubcategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value  | 
|
| 83 | 86 | 
    when 'fixed_version_id'  | 
| 84 | 87 | 
    v = Version.find_by_id(detail.value) and value = v.name if detail.value  | 
| 85 | 88 | 
    v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value  | 
| ... | ... | |
| 150 | 153 | 
    l(:field_subject),  | 
| 151 | 154 | 
    l(:field_assigned_to),  | 
| 152 | 155 | 
    l(:field_category),  | 
| 156 | 
    l(:field_sub_category),  | 
|
| 153 | 157 | 
    l(:field_fixed_version),  | 
| 154 | 158 | 
    l(:field_author),  | 
| 155 | 159 | 
    l(:field_start_date),  | 
| ... | ... | |
| 176 | 180 | 
    issue.subject,  | 
| 177 | 181 | 
    issue.assigned_to,  | 
| 178 | 182 | 
    issue.category,  | 
| 183 | 
    issue.subcategory,  | 
|
| 179 | 184 | 
    issue.fixed_version,  | 
| 180 | 185 | 
    issue.author.name,  | 
| 181 | 186 | 
    format_date(issue.start_date),  | 
| app/models/issue_category.rb (working copy) | ||
|---|---|---|
| 18 | 18 | 
    class IssueCategory < ActiveRecord::Base  | 
| 19 | 19 | 
    belongs_to :project  | 
| 20 | 20 | 
    belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'  | 
| 21 | 
    has_many :subcategories, :class_name => 'IssueSubcategory', :foreign_key => 'category_id', :dependent => :nullify  | 
|
| 21 | 22 | 
    has_many :issues, :foreign_key => 'category_id', :dependent => :nullify  | 
| 22 | 23 | 
     | 
| 23 | 24 | 
    validates_presence_of :name  | 
| 24 | 25 | 
    validates_uniqueness_of :name, :scope => [:project_id]  | 
| 25 | 26 | 
    validates_length_of :name, :maximum => 30  | 
| 27 | ||
| 28 | 
    # after something changes, we need to save the changes!  | 
|
| 29 | 
    after_update :save_sub_categories  | 
|
| 26 | 30 | 
     | 
| 27 | 31 | 
    alias :destroy_without_reassign :destroy  | 
| 28 | 32 | 
     | 
| ... | ... | |
| 34 | 38 | 
    end  | 
| 35 | 39 | 
    destroy_without_reassign  | 
| 36 | 40 | 
    end  | 
| 37 | 
     | 
|
| 41 | ||
| 42 | 
    # we get passed a hash of key => value pairs that describe  | 
|
| 43 | 
    # the new subcategories. we need to set their ID to this  | 
|
| 44 | 
    # objects ID  | 
|
| 45 | 
    def new_issue_subcategories=(subcategory_elements)  | 
|
| 46 | 
    subcategory_elements.each do |subcat|  | 
|
| 47 | 
    subcat[:category_id] = :id  | 
|
| 48 | 
    subcategories.build(subcat)  | 
|
| 49 | 
    end  | 
|
| 50 | 
    end  | 
|
| 51 | ||
| 52 | 
    # update or delete existing address lines  | 
|
| 53 | 
    def existing_issue_subcategories=(subcategory_elements)  | 
|
| 54 | 
    subcategories.reject(&:new_record?).each do |line|  | 
|
| 55 | 
    attributes = subcategory_elements[line.id.to_s]  | 
|
| 56 | 
    if attributes  | 
|
| 57 | 
    line.attributes = attributes  | 
|
| 58 | 
    else  | 
|
| 59 | 
    subcategories.delete(line)  | 
|
| 60 | 
    end  | 
|
| 61 | 
    end  | 
|
| 62 | 
    end  | 
|
| 63 | ||
| 38 | 64 | 
    def <=>(category)  | 
| 39 | 65 | 
    name <=> category.name  | 
| 40 | 66 | 
    end  | 
| 41 | 67 | 
     | 
| 42 | 68 | 
    def to_s; name end  | 
| 69 | ||
| 70 | 
    protected  | 
|
| 71 | 
    #called after a change (add/update/delete) to save to the database  | 
|
| 72 | 
    def save_sub_categories  | 
|
| 73 | 
    subcategories.each do |subcategory|  | 
|
| 74 | 
    subcategory.save(false)  | 
|
| 75 | 
    end  | 
|
| 76 | 
    end  | 
|
| 43 | 77 | 
    end  | 
| app/models/issue.rb (working copy) | ||
|---|---|---|
| 24 | 24 | 
    belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'  | 
| 25 | 25 | 
    belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'  | 
| 26 | 26 | 
    belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'  | 
| 27 | 
    belongs_to :subcategory, :class_name => 'IssueSubcategory', :foreign_key => 'subcategory_id'  | 
|
| 27 | 28 | |
| 28 | 29 | 
    has_many :journals, :as => :journalized, :dependent => :destroy  | 
| 29 | 30 | 
    has_many :time_entries, :dependent => :delete_all  | 
| app/models/issue_subcategory.rb (revision 0) | ||
|---|---|---|
| 1 | 
    class IssueSubcategory < ActiveRecord::Base  | 
|
| 2 | 
    belongs_to :issue_category, :foreign_key => 'category_id'  | 
|
| 3 | 
    validates_presence_of :name  | 
|
| 4 | 
    validates_uniqueness_of :name, :scope => [:category_id]  | 
|
| 5 | 
    validates_length_of :name, :maximum => 30  | 
|
| 6 | ||
| 7 | 
    def <=>(subcategory)  | 
|
| 8 | 
    name <=> subcategory.name  | 
|
| 9 | 
    end  | 
|
| 10 | ||
| 11 | 
    def to_s; name end  | 
|
| 12 | 
    end  | 
|
| app/controllers/issues_controller.rb (working copy) | ||
|---|---|---|
| 21 | 21 | 
    before_filter :find_issue, :only => [:show, :edit, :reply]  | 
| 22 | 22 | 
    before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]  | 
| 23 | 23 | 
    before_filter :find_project, :only => [:new, :update_form, :preview]  | 
| 24 | 
    before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]  | 
|
| 24 | 
      before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :update_issue_subcategories]
   | 
|
| 25 | 25 | 
    before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]  | 
| 26 | 26 | 
    accept_key_auth :index, :changes  | 
| 27 | 27 | |
| ... | ... | |
| 43 | 43 | 
    helper :timelog  | 
| 44 | 44 | 
    include Redmine::Export::PDF  | 
| 45 | 45 | |
| 46 | 
    def update_issue_subcategories  | 
|
| 47 | 
        results = IssueSubcategory.find(:all, :conditions=>{"category_id"=> params[:category_id]})
   | 
|
| 48 | 
    render :update do |page|  | 
|
| 49 | 
    page.replace_html 'issue_subcategory', :partial => 'subcategories', :object => results  | 
|
| 50 | 
    end  | 
|
| 51 | ||
| 52 | 
    end  | 
|
| 53 | ||
| 46 | 54 | 
    def index  | 
| 47 | 55 | 
    retrieve_query  | 
| 48 | 56 | 
    sort_init 'id', 'desc'  | 
| app/views/issue_categories/_subcategories.rhtml (revision 0) | ||
|---|---|---|
| 1 | 
    <span class="issue_subcategory">  | 
|
| 2 | 
    <% new_or_existing = subcategories.new_record? ? 'new' : 'existing' %>  | 
|
| 3 | 
    <% prefix = "category[#{new_or_existing}_issue_subcategories][]" %>
   | 
|
| 4 | 
    <% fields_for prefix, subcategories do |subcategory_form| -%>  | 
|
| 5 | 
    <p>  | 
|
| 6 | 
    <%= subcategory_form.text_field :name %>  | 
|
| 7 | 
    <%= link_to_function l(:button_delete) , "$(this).up('.issue_subcategory').remove()" %>
   | 
|
| 8 | 
    </p>  | 
|
| 9 | 
    <% end %>  | 
|
| 10 | 
    </span>  | 
|
| app/views/issue_categories/_form.rhtml (working copy) | ||
|---|---|---|
| 3 | 3 | 
    <div class="box">  | 
| 4 | 4 | 
    <p><%= f.text_field :name, :size => 30, :required => true %></p>  | 
| 5 | 5 | 
    <p><%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
   | 
| 6 | 
    <p>  | 
|
| 7 | 
    <%= add_issue_subcategory_link l(:label_issue_subcategory_new) %>  | 
|
| 8 | 
    <div id="issue_subcategories">  | 
|
| 9 | 
    <%= render :partial => 'subcategories', :collection => @category.subcategories %>  | 
|
| 6 | 10 | 
    </div>  | 
| 11 | ||
| 12 | 
    </p>  | 
|
| 13 | ||
| 14 | 
    </div>  | 
|
| app/views/issues/_subcategories.rhtml (revision 0) | ||
|---|---|---|
| 1 | 
    <%= subcategories.nil? ? "" : collection_select(:issue, :subcategory_id, subcategories, :id, :name,:include_blank=>true) %>  | 
|
| app/views/issues/_form.rhtml (working copy) | ||
|---|---|---|
| 1 | 1 | 
    <% if @issue.new_record? %>  | 
| 2 | 2 | 
    <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
   | 
| 3 | 3 | 
    <%= observe_field :issue_tracker_id, :url => { :action => :new },
   | 
| 4 | 
                                         :update => :content,
   | 
|
| 5 | 
                                         :with => "Form.serialize('issue-form')" %>
   | 
|
| 4 | 
    :update => :content,  | 
|
| 5 | 
    :with => "Form.serialize('issue-form')" %>
   | 
|
| 6 | 6 | 
    <hr />  | 
| 7 | 7 | 
    <% end %>  | 
| 8 | 8 | |
| ... | ... | |
| 26 | 26 | 
    <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
   | 
| 27 | 27 | 
    <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
   | 
| 28 | 28 | 
    <% unless @project.issue_categories.empty? %>  | 
| 29 | 
    <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
   | 
|
| 30 | 
    <%= prompt_to_remote(l(:label_issue_category_new),  | 
|
| 31 | 
    l(:label_issue_category_new), 'category[name]',  | 
|
| 32 | 
                         {:controller => 'projects', :action => 'add_issue_category', :id => @project},
   | 
|
| 33 | 
                         :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
   | 
|
| 29 | 
    <p>  | 
|
| 30 | 
        <%= f.select(:category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), {:include_blank => true}, {:onchange => "#{remote_function(:url  => {:action => "update_issue_subcategories"},
   | 
|
| 31 | 
    :with => "'category_id='+value")}"} )%>  | 
|
| 32 | 
    <span id="issue_subcategory">  | 
|
| 33 | 
    <% unless @issue.category.nil? %>  | 
|
| 34 | 
    <%= f.collection_select(:subcategory_id, @issue.category.subcategories, :id, :name,:include_blank=>true) %>  | 
|
| 34 | 35 | 
    <% end %>  | 
| 35 | 
    <%= content_tag('p', f.select(:fixed_version_id, 
   | 
|
| 36 | 
                                  (@project.versions.sort.collect {|v| [v.name, v.id]}),
   | 
|
| 37 | 
                                  { :include_blank => true })) unless @project.versions.empty? %>
   | 
|
| 36 | 
    </span>  | 
|
| 37 | 
    <%= prompt_to_remote(l(:label_issue_category_new),  | 
|
| 38 | 
    l(:label_issue_category_new), 'category[name]',  | 
|
| 39 | 
    {:controller => 'projects', :action => 'add_issue_category', :id => @project},
   | 
|
| 40 | 
    :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %>
   | 
|
| 41 | 
    </p>  | 
|
| 42 | 
    <% end %>  | 
|
| 43 | 
    <%= content_tag('p', f.select(:fixed_version_id,
   | 
|
| 44 | 
    (@project.versions.sort.collect {|v| [v.name, v.id]}),
   | 
|
| 45 | 
    { :include_blank => true })) unless @project.versions.empty? %>
   | 
|
| 38 | 46 | 
    </div>  | 
| 39 | 47 | |
| 40 | 48 | 
    <div class="splitcontentright">  | 
| app/views/issues/show.rhtml (working copy) | ||
|---|---|---|
| 31 | 31 | 
        <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>
   | 
| 32 | 32 | 
    </tr>  | 
| 33 | 33 | 
    <tr>  | 
| 34 | 
    <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>  | 
|
| 34 | 
    <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %>  | 
|
| 35 | 
    <%=h @issue.subcategory ? " / "+@issue.subcategory.name : "-" %></td>  | 
|
| 35 | 36 | 
    <% if User.current.allowed_to?(:view_time_entries, @project) %>  | 
| 36 | 37 | 
    <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>  | 
| 37 | 38 | 
        <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>
   | 
| lang/en.yml (working copy) | ||
|---|---|---|
| 122 | 122 | 
    field_max_length: Maximum length  | 
| 123 | 123 | 
    field_value: Value  | 
| 124 | 124 | 
    field_category: Category  | 
| 125 | 
    field_subcategory: Subcategory  | 
|
| 125 | 126 | 
    field_title: Title  | 
| 126 | 127 | 
    field_project: Project  | 
| 127 | 128 | 
    field_issue: Issue  | 
| ... | ... | |
| 321 | 322 | 
    label_issue_status_plural: Issue statuses  | 
| 322 | 323 | 
    label_issue_status_new: New status  | 
| 323 | 324 | 
    label_issue_category: Issue category  | 
| 325 | 
    label_issue_subcategory: Issue subcategory  | 
|
| 324 | 326 | 
    label_issue_category_plural: Issue categories  | 
| 327 | 
    label_issue_subcategory_plural: Issue subcategories  | 
|
| 325 | 328 | 
    label_issue_category_new: New category  | 
| 329 | 
    label_issue_subcategory_new: New subcategory  | 
|
| 326 | 330 | 
    label_custom_field: Custom field  | 
| 327 | 331 | 
    label_custom_field_plural: Custom fields  | 
| 328 | 332 | 
    label_custom_field_new: New custom field  | 
| db/migrate/20090125190215_create_issue_category_subcategories.rb (revision 0) | ||
|---|---|---|
| 1 | 
    class CreateIssueCategorySubcategories < ActiveRecord::Migration  | 
|
| 2 | 
    def self.up  | 
|
| 3 | 
    create_table :issue_subcategories do |t|  | 
|
| 4 | 
    t.integer :category_id  | 
|
| 5 | 
    t.string :name, :limit => 30, :null => false  | 
|
| 6 | 
    t.timestamps  | 
|
| 7 | 
    end  | 
|
| 8 | 
    end  | 
|
| 9 | ||
| 10 | 
    def self.down  | 
|
| 11 | 
    drop_table :issue_subcategories  | 
|
| 12 | 
    end  | 
|
| 13 | 
    end  | 
|
| db/migrate/20090125191829_add_sub_categories_to_issues.rb (revision 0) | ||
|---|---|---|
| 1 | 
    class AddSubCategoriesToIssues < ActiveRecord::Migration  | 
|
| 2 | 
    def self.up  | 
|
| 3 | 
    add_column :issues, :subcategory_id, :integer  | 
|
| 4 | 
    end  | 
|
| 5 | ||
| 6 | 
    def self.down  | 
|
| 7 | 
    remove_column :issues, :subcategory_id  | 
|
| 8 | 
    end  | 
|
| 9 | 
    end  | 
|