Feature #28198 » 0002-Support-issue-relations-when-importing-issues.patch
| app/models/import.rb | ||
|---|---|---|
| 187 | 187 | 
    item.save!  | 
| 188 | 188 | 
    imported += 1  | 
| 189 | 189 | |
| 190 | 
    extend_object(row, item, object) if object.persisted?  | 
|
| 190 | 191 | 
    do_callbacks(use_unique_id? ? item.unique_id : item.position, object)  | 
| 191 | 192 | 
    end  | 
| 192 | 193 | 
    current = position  | 
| ... | ... | |
| 244 | 245 | |
| 245 | 246 | 
    # Builds a record for the given row and returns it  | 
| 246 | 247 | 
    # To be implemented by subclasses  | 
| 247 | 
    def build_object(row)  | 
|
| 248 | 
    def build_object(row, item)  | 
|
| 249 | 
    end  | 
|
| 250 | ||
| 251 | 
    # Extends object with properties, that may only be handled after it's been  | 
|
| 252 | 
    # persisted.  | 
|
| 253 | 
    def extend_object(row, item, object)  | 
|
| 248 | 254 | 
    end  | 
| 249 | 255 | |
| 250 | 256 | 
    # Generates a filename used to store the import file  | 
| app/models/issue_import.rb | ||
|---|---|---|
| 200 | 200 | 
    issue  | 
| 201 | 201 | 
    end  | 
| 202 | 202 | |
| 203 | 
    def extend_object(row, item, issue)  | 
|
| 204 | 
    build_relations(row, item, issue)  | 
|
| 205 | 
    end  | 
|
| 206 | ||
| 207 | 
    def build_relations(row, item, issue)  | 
|
| 208 | 
    IssueRelation::TYPES.keys.each do |type|  | 
|
| 209 | 
    has_delay = type == IssueRelation::TYPE_PRECEDES || type == IssueRelation::TYPE_FOLLOWS  | 
|
| 210 | ||
| 211 | 
          if decls = relation_values(row, "relation_#{type}")
   | 
|
| 212 | 
    decls.each do |decl|  | 
|
| 213 | 
    unless decl[:matches]  | 
|
| 214 | 
    # Invalid relation syntax - doesn't match regexp  | 
|
| 215 | 
    next  | 
|
| 216 | 
    end  | 
|
| 217 | ||
| 218 | 
    if decl[:delay] && !has_delay  | 
|
| 219 | 
    # Invalid relation syntax - delay for relation that doesn't support delays  | 
|
| 220 | 
    next  | 
|
| 221 | 
    end  | 
|
| 222 | ||
| 223 | 
    relation = IssueRelation.new(  | 
|
| 224 | 
    "relation_type" => type,  | 
|
| 225 | 
    "issue_from_id" => issue.id  | 
|
| 226 | 
    )  | 
|
| 227 | ||
| 228 | 
    if decl[:other_id]  | 
|
| 229 | 
    relation.issue_to_id = decl[:other_id]  | 
|
| 230 | 
    elsif decl[:other_pos]  | 
|
| 231 | 
    if use_unique_id?  | 
|
| 232 | 
    issue_id = items.where(:unique_id => decl[:other_pos]).first.try(:obj_id)  | 
|
| 233 | 
    if issue_id  | 
|
| 234 | 
    relation.issue_to_id = issue_id  | 
|
| 235 | 
    else  | 
|
| 236 | 
    add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])  | 
|
| 237 | 
    next  | 
|
| 238 | 
    end  | 
|
| 239 | 
    else  | 
|
| 240 | 
    if decl[:other_pos] > item.position  | 
|
| 241 | 
    add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])  | 
|
| 242 | 
    next  | 
|
| 243 | 
    elsif issue_id = items.where(:position => decl[:other_pos]).first.try(:obj_id)  | 
|
| 244 | 
    relation.issue_to_id = issue_id  | 
|
| 245 | 
    end  | 
|
| 246 | 
    end  | 
|
| 247 | 
    end  | 
|
| 248 | ||
| 249 | 
    relation.delay = decl[:delay] if decl[:delay]  | 
|
| 250 | ||
| 251 | 
    relation.save!  | 
|
| 252 | 
    end  | 
|
| 253 | 
    end  | 
|
| 254 | 
    end  | 
|
| 255 | ||
| 256 | 
    issue  | 
|
| 257 | 
    end  | 
|
| 258 | ||
| 259 | 
    def relation_values(row, name)  | 
|
| 260 | 
    content = row_value(row, name)  | 
|
| 261 | ||
| 262 | 
    return if content.blank?  | 
|
| 263 | ||
| 264 | 
        content.split(",").map do |declaration|
   | 
|
| 265 | 
    declaration = declaration.strip  | 
|
| 266 | ||
| 267 | 
    # Valid expression:  | 
|
| 268 | 
    #  | 
|
| 269 | 
    # 123 => row 123 within the CSV  | 
|
| 270 | 
    # #123 => issue with ID 123  | 
|
| 271 | 
    #  | 
|
| 272 | 
    # For precedes and follows  | 
|
| 273 | 
    #  | 
|
| 274 | 
    # 123 7d => row 123 within CSV with 7 day delay  | 
|
| 275 | 
    # #123 7d => issue with ID 123 with 7 day delay  | 
|
| 276 | 
    # 123 -3d => negative delay allowed  | 
|
| 277 | 
    #  | 
|
| 278 | 
    #  | 
|
| 279 | 
    # Invalid expression:  | 
|
| 280 | 
    #  | 
|
| 281 | 
    # No. 123 => Invalid leading letters  | 
|
| 282 | 
    # # 123 => Invalid space between # and issue number  | 
|
| 283 | 
    # 123 8h => No other time units allowed (just days)  | 
|
| 284 | 
    #  | 
|
| 285 | 
    # Please note: If unique_id mapping is present, the whole line - but the  | 
|
| 286 | 
    # trailing delay expression - is considered unique_id.  | 
|
| 287 | 
    #  | 
|
| 288 | 
    # See examples at Rubular http://rubular.com/r/mgXM5Rp6zK  | 
|
| 289 | 
    #  | 
|
| 290 | 
    match = declaration.match(/\A(?<unique_id>(?<is_id>#)?(?<id>\d+)|.+?)(?:\s+(?<delay>-?\d+)d)?\z/)  | 
|
| 291 | ||
| 292 | 
          result = {
   | 
|
| 293 | 
    :matches => false,  | 
|
| 294 | 
    :declaration => declaration  | 
|
| 295 | 
    }  | 
|
| 296 | ||
| 297 | 
    if match  | 
|
| 298 | 
    result[:matches] = true  | 
|
| 299 | 
    result[:delay] = match[:delay]  | 
|
| 300 | ||
| 301 | 
    if match[:is_id] and match[:id]  | 
|
| 302 | 
    result[:other_id] = match[:id]  | 
|
| 303 | 
    elsif use_unique_id? and match[:unique_id]  | 
|
| 304 | 
    result[:other_pos] = match[:unique_id]  | 
|
| 305 | 
    elsif match[:id]  | 
|
| 306 | 
    result[:other_pos] = match[:id].to_i  | 
|
| 307 | 
    else  | 
|
| 308 | 
    result[:matches] = false  | 
|
| 309 | 
    end  | 
|
| 310 | 
    end  | 
|
| 311 | ||
| 312 | 
    result  | 
|
| 313 | 
    end  | 
|
| 314 | 
    end  | 
|
| 315 | ||
| 203 | 316 | 
    # Callback that sets issue as the parent of a previously imported issue  | 
| 204 | 317 | 
    def set_as_parent_callback(issue, child_position)  | 
| 205 | 318 | 
    child_id = items.where(:position => child_position).first.try(:obj_id)  | 
| ... | ... | |
| 212 | 325 | 
    child.save!  | 
| 213 | 326 | 
    issue.reload  | 
| 214 | 327 | 
    end  | 
| 328 | ||
| 329 | 
    def set_relation_callback(to_issue, from_position, type, delay)  | 
|
| 330 | 
    return if to_issue.new_record?  | 
|
| 331 | ||
| 332 | 
    from_id = items.where(:position => from_position).first.try(:obj_id)  | 
|
| 333 | 
    return unless from_id  | 
|
| 334 | ||
| 335 | 
    IssueRelation.create!(  | 
|
| 336 | 
    'relation_type' => type,  | 
|
| 337 | 
    'issue_from_id' => from_id,  | 
|
| 338 | 
    'issue_to_id' => to_issue.id,  | 
|
| 339 | 
    'delay' => delay  | 
|
| 340 | 
    )  | 
|
| 341 | 
    to_issue.reload  | 
|
| 342 | 
    end  | 
|
| 215 | 343 | 
    end  | 
| app/views/imports/_fields_mapping.html.erb | ||
|---|---|---|
| 1 | 
    <div class="splitcontent">  | 
|
| 2 | 
    <div class="splitcontentleft">  | 
|
| 3 | 1 | 
    <p>  | 
| 4 | 2 | 
    <label for="import_mapping_project_id"><%= l(:label_project) %></label>  | 
| 5 | 3 | 
    <%= select_tag 'import_settings[mapping][project_id]',  | 
| ... | ... | |
| 15 | 13 | 
    <label for="import_mapping_status"><%= l(:field_status) %></label>  | 
| 16 | 14 | 
    <%= mapping_select_tag @import, 'status' %>  | 
| 17 | 15 | 
    </p>  | 
| 18 | 
    </div>  | 
|
| 19 | ||
| 20 | 
    <div class="splitcontentright">  | 
|
| 21 | 
    <p></p>  | 
|
| 22 | 
    <p>  | 
|
| 23 | 
    <label for="import_mapping_unique_id"><%= l(:field_unique_id) %></label>  | 
|
| 24 | 
    <%= mapping_select_tag @import, 'unique_id' %>  | 
|
| 25 | 
    </p>  | 
|
| 26 | 
    </div>  | 
|
| 27 | 
    </div>  | 
|
| 28 | 16 | |
| 29 | 17 | 
    <div class="splitcontent">  | 
| 30 | 18 | 
    <div class="splitcontentleft">  | 
| ... | ... | |
| 64 | 52 | 
    </label>  | 
| 65 | 53 | 
    <% end %>  | 
| 66 | 54 | 
    </p>  | 
| 67 | 
    <% @custom_fields.each do |field| %>  | 
|
| 68 | 
    <p>  | 
|
| 69 | 
    <label for="import_mapping_cf_<% field.id %>"><%= field.name %></label>  | 
|
| 70 | 
        <%= mapping_select_tag @import, "cf_#{field.id}" %>
   | 
|
| 71 | 
    </p>  | 
|
| 72 | 
    <% end %>  | 
|
| 73 | 55 | 
    </div>  | 
| 74 | 56 | |
| 75 | 57 | 
    <div class="splitcontentright">  | 
| ... | ... | |
| 77 | 59 | 
    <label for="import_mapping_is_private"><%= l(:field_is_private) %></label>  | 
| 78 | 60 | 
    <%= mapping_select_tag @import, 'is_private' %>  | 
| 79 | 61 | 
    </p>  | 
| 80 | 
    <p>  | 
|
| 81 | 
    <label for="import_mapping_parent_issue_id"><%= l(:field_parent_issue) %></label>  | 
|
| 82 | 
    <%= mapping_select_tag @import, 'parent_issue_id' %>  | 
|
| 83 | 
    </p>  | 
|
| 84 | 62 | 
    <p>  | 
| 85 | 63 | 
    <label for="import_mapping_start_date"><%= l(:field_start_date) %></label>  | 
| 86 | 64 | 
    <%= mapping_select_tag @import, 'start_date' %>  | 
| ... | ... | |
| 97 | 75 | 
    <label for="import_mapping_done_ratio"><%= l(:field_done_ratio) %></label>  | 
| 98 | 76 | 
    <%= mapping_select_tag @import, 'done_ratio' %>  | 
| 99 | 77 | 
    </p>  | 
| 78 | 
    <% @custom_fields.each do |field| %>  | 
|
| 79 | 
    <p>  | 
|
| 80 | 
    <label for="import_mapping_cf_<%= field.id %>"><%= field.name %></label>  | 
|
| 81 | 
        <%= mapping_select_tag @import, "cf_#{field.id}" %>
   | 
|
| 82 | 
    </p>  | 
|
| 83 | 
    <% end %>  | 
|
| 100 | 84 | 
    </div>  | 
| 101 | 85 | 
    </div>  | 
| 102 | 86 | |
| app/views/imports/_relations_mapping.html.erb | ||
|---|---|---|
| 1 | 
    <div class="splitcontent">  | 
|
| 2 | 
    <div class="splitcontentleft">  | 
|
| 3 | 
    <p>  | 
|
| 4 | 
    <label for="import_mapping_unique_id"><%= l(:field_unique_id) %></label>  | 
|
| 5 | 
    <%= mapping_select_tag @import, 'unique_id' %>  | 
|
| 6 | 
    </p>  | 
|
| 7 | 
    <p>  | 
|
| 8 | 
    <label for="import_settings_mapping_parent_issue_id"><%= l(:field_parent_issue) %></label>  | 
|
| 9 | 
    <%= mapping_select_tag @import, 'parent_issue_id' %>  | 
|
| 10 | 
    </p>  | 
|
| 11 | ||
| 12 | 
    <p>  | 
|
| 13 | 
    <label for="import_settings_mapping_relation_duplicates"><%= l(:label_duplicates) %></label>  | 
|
| 14 | 
    <%= mapping_select_tag @import, 'relation_duplicates' %>  | 
|
| 15 | 
    </p>  | 
|
| 16 | ||
| 17 | 
    <p>  | 
|
| 18 | 
    <label for="import_settings_mapping_relation_duplicated"><%= l(:label_duplicated_by) %></label>  | 
|
| 19 | 
    <%= mapping_select_tag @import, 'relation_duplicated' %>  | 
|
| 20 | 
    </p>  | 
|
| 21 | ||
| 22 | 
    <p>  | 
|
| 23 | 
    <label for="import_settings_mapping_relation_blocks"><%= l(:label_blocks) %></label>  | 
|
| 24 | 
    <%= mapping_select_tag @import, 'relation_blocks' %>  | 
|
| 25 | 
    </p>  | 
|
| 26 | ||
| 27 | 
    <p>  | 
|
| 28 | 
    <label for="import_settings_mapping_relation_blocked"><%= l(:label_blocked_by) %></label>  | 
|
| 29 | 
    <%= mapping_select_tag @import, 'relation_blocked' %>  | 
|
| 30 | 
    </p>  | 
|
| 31 | 
    </div>  | 
|
| 32 | ||
| 33 | 
    <div class="splitcontentright">  | 
|
| 34 | 
    <p></p>  | 
|
| 35 | 
    <p>  | 
|
| 36 | 
    <label for="import_settings_mapping_relation_relates"><%= l(:label_relates_to) %></label>  | 
|
| 37 | 
    <%= mapping_select_tag @import, 'relation_relates' %>  | 
|
| 38 | 
    </p>  | 
|
| 39 | ||
| 40 | 
    <p>  | 
|
| 41 | 
    <label for="import_settings_mapping_relation_precedes"><%= l(:label_precedes) %></label>  | 
|
| 42 | 
    <%= mapping_select_tag @import, 'relation_precedes' %>  | 
|
| 43 | 
    </p>  | 
|
| 44 | ||
| 45 | 
    <p>  | 
|
| 46 | 
    <label for="import_settings_mapping_relation_follows"><%= l(:label_follows) %></label>  | 
|
| 47 | 
    <%= mapping_select_tag @import, 'relation_follows' %>  | 
|
| 48 | 
    </p>  | 
|
| 49 | ||
| 50 | 
    <p>  | 
|
| 51 | 
    <label for="import_settings_mapping_relation_copied_to"><%= l(:label_copied_to) %></label>  | 
|
| 52 | 
    <%= mapping_select_tag @import, 'relation_copied_to' %>  | 
|
| 53 | 
    </p>  | 
|
| 54 | ||
| 55 | 
    <p>  | 
|
| 56 | 
    <label for="import_settings_mapping_relation_copied_from"><%= l(:label_copied_from) %></label>  | 
|
| 57 | 
    <%= mapping_select_tag @import, 'relation_copied_from' %>  | 
|
| 58 | 
    </p>  | 
|
| 59 | 
    </div>  | 
|
| 60 | 
    </div>  | 
|
| app/views/imports/mapping.html.erb | ||
|---|---|---|
| 8 | 8 | 
    </div>  | 
| 9 | 9 | 
    </fieldset>  | 
| 10 | 10 | |
| 11 | 
    <fieldset class="box tabular collapsible collapsed">  | 
|
| 12 | 
    <legend onclick="toggleFieldset(this);"><%= l(:label_relations_mapping) %></legend>  | 
|
| 13 | 
    <div id="relations-mapping" style="display: none;">  | 
|
| 14 | 
    <%= render :partial => 'relations_mapping' %>  | 
|
| 15 | 
    </div>  | 
|
| 16 | 
    </fieldset>  | 
|
| 17 | ||
| 11 | 18 | 
    <div class="autoscroll">  | 
| 12 | 19 | 
    <fieldset class="box">  | 
| 13 | 20 | 
    <legend><%= l(:label_file_content_preview) %></legend>  | 
| config/locales/de.yml | ||
|---|---|---|
| 1183 | 1183 | 
    label_quote_char: Anführungszeichen  | 
| 1184 | 1184 | 
    label_double_quote_char: Doppelte Anführungszeichen  | 
| 1185 | 1185 | 
    label_fields_mapping: Zuordnung der Felder  | 
| 1186 | 
    label_relations_mapping: Zuordnung von Beziehungen  | 
|
| 1186 | 1187 | 
    label_file_content_preview: Inhaltsvorschau  | 
| 1187 | 1188 | 
    label_create_missing_values: Ergänze fehlende Werte  | 
| 1188 | 1189 | 
    button_import: Importieren  | 
| config/locales/en.yml | ||
|---|---|---|
| 1013 | 1013 | 
    label_quote_char: Quote  | 
| 1014 | 1014 | 
    label_double_quote_char: Double quote  | 
| 1015 | 1015 | 
    label_fields_mapping: Fields mapping  | 
| 1016 | 
    label_relations_mapping: Relations mapping  | 
|
| 1016 | 1017 | 
    label_file_content_preview: File content preview  | 
| 1017 | 1018 | 
    label_create_missing_values: Create missing values  | 
| 1018 | 1019 | 
    label_api: API  | 
| test/fixtures/files/import_subtasks.csv | ||
|---|---|---|
| 1 | 
    row;tracker;subject;parent  | 
|
| 2 | 
    1;bug;Root;  | 
|
| 3 | 
    2;bug;Child 1;1  | 
|
| 4 | 
    3;bug;Grand-child;4  | 
|
| 5 | 
    4;bug;Child 2;1  | 
|
| 1 | 
    row;tracker;subject;parent;simple relation;delayed relation  | 
|
| 2 | 
    1;bug;Root;;;  | 
|
| 3 | 
    2;bug;Child 1;1;1,4;1 2d  | 
|
| 4 | 
    3;bug;Grand-child;4;4;4 -1d  | 
|
| 5 | 
    4;bug;Child 2;1;1;1 1d  | 
|
| test/fixtures/files/import_subtasks_with_relations.csv | ||
|---|---|---|
| 1 | 
    row;tracker;subject;start;due;parent;follows  | 
|
| 2 | 
    1;bug;2nd Child;2020-01-12;2020-01-20;3;2 1d  | 
|
| 3 | 
    2;bug;1st Child;2020-01-01;2020-01-10;3;  | 
|
| 4 | 
    3;bug;Parent;2020-01-01;2020-01-31;;  | 
|
| 5 | 
    1;bug;3rd Child;2020-01-22;2020-01-31;3;1 1d  | 
|
| test/fixtures/files/import_subtasks_with_unique_id.csv | ||
|---|---|---|
| 1 | 
    id;tracker;subject;parent  | 
|
| 2 | 
    RED-I;bug;Root;  | 
|
| 3 | 
    RED-II;bug;Child 1;RED-I  | 
|
| 4 | 
    RED-III;bug;Grand-child;RED-IV  | 
|
| 5 | 
    RED-IV;bug;Child 2;RED-I  | 
|
| 1 | 
    id;tracker;subject;parent;follows  | 
|
| 2 | 
    RED-IV;bug;Grand-child;RED-III;  | 
|
| 3 | 
    RED-III;bug;Child 2;RED-I;RED-II 1d  | 
|
| 4 | 
    RED-II;bug;Child 1;RED-I;  | 
|
| 5 | 
    RED-I;bug;Root;;  | 
|
| 6 | 
    BLUE-I;bug;Root;;  | 
|
| 7 | 
    BLUE-II;bug;Child 1;BLUE-I;  | 
|
| 8 | 
    BLUE-III;bug;Child 2;BLUE-I;BLUE-II 1d  | 
|
| 9 | 
    BLUE-IV;bug;Grand-child;BLUE-III;  | 
|
| 10 | 
    GREEN-II;bug;Thing;#1;#2 3d;  | 
|
| test/unit/issue_import_test.rb | ||
|---|---|---|
| 128 | 128 | 
    assert_equal child2, grandchild.parent  | 
| 129 | 129 | 
    end  | 
| 130 | 130 | |
| 131 | 
      def test_backward_and_forward_reference_with_unique_id
   | 
|
| 131 | 
      def test_references_with_unique_id
   | 
|
| 132 | 132 | 
        import = generate_import_with_mapping('import_subtasks_with_unique_id.csv')
   | 
| 133 | 
        import.settings['mapping'] = {'project_id' => '1', 'unique_id' => '0', 'tracker' => '1', 'subject' => '2', 'parent_issue_id' => '3'}
   | 
|
| 133 | 
        import.settings['mapping'] = {'project_id' => '1', 'unique_id' => '0', 'tracker' => '1', 'subject' => '2', 'parent_issue_id' => '3', 'relation_follows' => '4'}
   | 
|
| 134 | 134 | 
    import.save!  | 
| 135 | 135 | |
| 136 | 
        root, child1, grandchild, child2 = new_records(Issue, 4) { import.run }
   | 
|
| 137 | 
    assert_equal root, child1.parent  | 
|
| 138 | 
    assert_equal child2, grandchild.parent  | 
|
| 136 | 
        red4, red3, red2, red1, blue1, blue2, blue3, blue4, green = new_records(Issue, 9) { import.run }
   | 
|
| 137 | ||
| 138 | 
    # future references  | 
|
| 139 | 
    assert_equal red1, red2.parent  | 
|
| 140 | 
    assert_equal red3, red4.parent  | 
|
| 141 | ||
| 142 | 
        assert IssueRelation.where('issue_from_id' => red2.id, 'issue_to_id' => red3.id, 'delay' => 1, 'relation_type' => 'precedes').present?
   | 
|
| 143 | ||
| 144 | 
    # past references  | 
|
| 145 | 
    assert_equal blue1, blue2.parent  | 
|
| 146 | 
    assert_equal blue3, blue4.parent  | 
|
| 147 | ||
| 148 | 
        assert IssueRelation.where('issue_from_id' => blue2.id, 'issue_to_id' => blue3.id, 'delay' => 1, 'relation_type' => 'precedes').present?
   | 
|
| 149 | ||
| 150 | 
    assert_equal issues(:issues_001), green.parent  | 
|
| 151 | 
        assert IssueRelation.where('issue_from_id' => issues(:issues_002).id, 'issue_to_id' => green.id, 'delay' => 3, 'relation_type' => 'precedes').present?
   | 
|
| 152 | 
    end  | 
|
| 153 | ||
| 154 | 
    def test_follow_relation  | 
|
| 155 | 
        import = generate_import_with_mapping('import_subtasks.csv')
   | 
|
| 156 | 
        import.settings['mapping'] = {'project_id' => '1', 'tracker' => '1', 'subject' => '2', 'relation_relates' => '4'}
   | 
|
| 157 | 
    import.save!  | 
|
| 158 | ||
| 159 | 
        one, one_one, one_two_one, one_two = new_records(Issue, 4) { import.run }
   | 
|
| 160 | 
    assert_equal 2, one.relations.count  | 
|
| 161 | 
        assert one.relations.all? { |r| r.relation_type == 'relates' }
   | 
|
| 162 | 
        assert one.relations.any? { |r| r.other_issue(one) == one_one }
   | 
|
| 163 | 
        assert one.relations.any? { |r| r.other_issue(one) == one_two }
   | 
|
| 164 | ||
| 165 | 
    assert_equal 2, one_one.relations.count  | 
|
| 166 | 
        assert one_one.relations.all? { |r| r.relation_type == 'relates' }
   | 
|
| 167 | 
        assert one_one.relations.any? { |r| r.other_issue(one_one) == one }
   | 
|
| 168 | 
        assert one_one.relations.any? { |r| r.other_issue(one_one) == one_two }
   | 
|
| 169 | ||
| 170 | 
    assert_equal 3, one_two.relations.count  | 
|
| 171 | 
        assert one_two.relations.all? { |r| r.relation_type == 'relates' }
   | 
|
| 172 | 
        assert one_two.relations.any? { |r| r.other_issue(one_two) == one }
   | 
|
| 173 | 
        assert one_two.relations.any? { |r| r.other_issue(one_two) == one_one }
   | 
|
| 174 | 
        assert one_two.relations.any? { |r| r.other_issue(one_two) == one_two_one }
   | 
|
| 175 | ||
| 176 | 
    assert_equal 1, one_two_one.relations.count  | 
|
| 177 | 
        assert one_two_one.relations.all? { |r| r.relation_type == 'relates' }
   | 
|
| 178 | 
        assert one_two_one.relations.any? { |r| r.other_issue(one_two_one) == one_two }
   | 
|
| 179 | 
    end  | 
|
| 180 | ||
| 181 | 
    def test_delayed_relation  | 
|
| 182 | 
        import = generate_import_with_mapping('import_subtasks.csv')
   | 
|
| 183 | 
        import.settings['mapping'] = {'project_id' => '1', 'tracker' => '1', 'subject' => '2', 'relation_precedes' => '5'}
   | 
|
| 184 | 
    import.save!  | 
|
| 185 | ||
| 186 | 
        one, one_one, one_two_one, one_two = new_records(Issue, 4) { import.run }
   | 
|
| 187 | ||
| 188 | 
    assert_equal 2, one.relations_to.count  | 
|
| 189 | 
        assert one.relations_to.all? { |r| r.relation_type == 'precedes' }
   | 
|
| 190 | 
        assert one.relations_to.any? { |r| r.issue_from == one_one && r.delay == 2 }
   | 
|
| 191 | 
        assert one.relations_to.any? { |r| r.issue_from == one_two && r.delay == 1 }
   | 
|
| 192 | ||
| 193 | ||
| 194 | 
    assert_equal 1, one_one.relations_from.count  | 
|
| 195 | 
        assert one_one.relations_from.all? { |r| r.relation_type == 'precedes' }
   | 
|
| 196 | 
        assert one_one.relations_from.any? { |r| r.issue_to == one && r.delay == 2 }
   | 
|
| 197 | ||
| 198 | ||
| 199 | 
    assert_equal 1, one_two.relations_to.count  | 
|
| 200 | 
        assert one_two.relations_to.all? { |r| r.relation_type == 'precedes' }
   | 
|
| 201 | 
        assert one_two.relations_to.any? { |r| r.issue_from == one_two_one && r.delay == -1 }
   | 
|
| 202 | ||
| 203 | 
    assert_equal 1, one_two.relations_from.count  | 
|
| 204 | 
        assert one_two.relations_from.all? { |r| r.relation_type == 'precedes' }
   | 
|
| 205 | 
        assert one_two.relations_from.any? { |r| r.issue_to == one && r.delay == 1 }
   | 
|
| 206 | ||
| 207 | ||
| 208 | 
    assert_equal 1, one_two_one.relations_from.count  | 
|
| 209 | 
        assert one_two_one.relations_from.all? { |r| r.relation_type == 'precedes' }
   | 
|
| 210 | 
        assert one_two_one.relations_from.any? { |r| r.issue_to == one_two && r.delay == -1 }
   | 
|
| 211 | 
    end  | 
|
| 212 | ||
| 213 | 
    def test_parent_and_follows_relation  | 
|
| 214 | 
        import = generate_import_with_mapping('import_subtasks_with_relations.csv')
   | 
|
| 215 | 
        import.settings['mapping'] = {
   | 
|
| 216 | 
    'project_id' => '1',  | 
|
| 217 | 
    'tracker' => '1',  | 
|
| 218 | ||
| 219 | 
    'subject' => '2',  | 
|
| 220 | 
    'start_date' => '3',  | 
|
| 221 | 
    'due_date' => '4',  | 
|
| 222 | 
    'parent_issue_id' => '5',  | 
|
| 223 | 
    'relation_follows' => '6'  | 
|
| 224 | 
    }  | 
|
| 225 | 
    import.save!  | 
|
| 226 | ||
| 227 | 
        second, first, parent, third = assert_difference('IssueRelation.count', 2) { new_records(Issue, 4) { import.run } }
   | 
|
| 228 | ||
| 229 | 
    # Parent relations  | 
|
| 230 | 
    assert_equal parent, first.parent  | 
|
| 231 | 
    assert_equal parent, second.parent  | 
|
| 232 | 
    assert_equal parent, third.parent  | 
|
| 233 | ||
| 234 | 
    # Issue relations  | 
|
| 235 | 
    assert IssueRelation.where(  | 
|
| 236 | 
    :issue_from_id => first.id,  | 
|
| 237 | 
    :issue_to_id => second.id,  | 
|
| 238 | 
    :relation_type => 'precedes',  | 
|
| 239 | 
    :delay => 1).present?  | 
|
| 240 | ||
| 241 | 
    assert IssueRelation.where(  | 
|
| 242 | 
    :issue_from_id => second.id,  | 
|
| 243 | 
    :issue_to_id => third.id,  | 
|
| 244 | 
    :relation_type => 'precedes',  | 
|
| 245 | 
    :delay => 1).present?  | 
|
| 246 | ||
| 247 | ||
| 248 | 
    # Checking dates, because they might act weird, when relations are added  | 
|
| 249 | 
    assert_equal Date.new(2020, 1, 1), parent.start_date  | 
|
| 250 | 
    assert_equal Date.new(2020, 1, 31), parent.due_date  | 
|
| 251 | ||
| 252 | 
    assert_equal Date.new(2020, 1, 1), first.start_date  | 
|
| 253 | 
    assert_equal Date.new(2020, 1, 10), first.due_date  | 
|
| 254 | ||
| 255 | 
    assert_equal Date.new(2020, 1, 12), second.start_date  | 
|
| 256 | 
    assert_equal Date.new(2020, 1, 20), second.due_date  | 
|
| 257 | ||
| 258 | 
    assert_equal Date.new(2020, 1, 22), third.start_date  | 
|
| 259 | 
    assert_equal Date.new(2020, 1, 31), third.due_date  | 
|
| 139 | 260 | 
    end  | 
| 140 | 261 | |
| 141 | 262 | 
    def test_assignee_should_be_set  |