--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueRelationsController < ApplicationController
+ layout 'base'
+ before_filter :find_project, :authorize
+
+ def new
+ @relation = IssueRelation.new(params[:relation])
+ @relation.issue_from = @issue
+ @relation.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
+ format.js do
+ render :update do |page|
+ page.replace_html "relations", :partial => 'issues/relations'
+ if @relation.errors.empty?
+ page << "$('relation_delay').value = ''"
+ page << "$('relation_issue_to_id').value = ''"
+ end
+ end
+ end
+ end
+ end
+
+ def destroy
+ relation = IssueRelation.find(params[:id])
+ if request.post? && @issue.relations.include?(relation)
+ relation.destroy
+ @issue.reload
+ end
+ respond_to do |format|
+ format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
+ format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
+ end
+ end
+
+private
+ def find_project
+ @issue = Issue.find(params[:issue_id])
+ @project = @issue.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
+ helper :issue_relations
+ include IssueRelationsHelper
def show
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
unless i.project_id == new_project.id
i.category = nil
i.fixed_version = nil
+ # delete issue relations
+ i.relations_from.clear
+ i.relations_to.clear
end
# move the issue
i.project = new_project
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module IssueRelationsHelper
+ def collection_for_relation_type_select
+ values = IssueRelation::TYPES
+ values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
+ end
+end
# redMine - project management software
-# Copyright (C) 2006 Jean-Philippe Lang
+# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Issue < ActiveRecord::Base
-
belongs_to :project
belongs_to :tracker
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
has_many :custom_fields, :through => :custom_values
has_and_belongs_to_many :changesets, :order => "revision ASC"
+ has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
+ has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
+
acts_as_watchable
validates_presence_of :subject, :description, :priority, :tracker, :author, :status
if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :activerecord_error_greater_than_start_date
end
+
+ if start_date && soonest_start && start_date < soonest_start
+ errors.add :start_date, :activerecord_error_invalid
+ end
end
-
- #def before_create
- # build_history
- #end
- def before_save
+ def before_save
if @current_journal
# attributes changes
(Issue.column_names - %w(id description)).each {|c|
end
end
+ def after_save
+ relations_from.each(&:set_issue_to_dates)
+ end
+
def long_id
"%05d" % self.id
end
def spent_hours
@spent_hours ||= time_entries.sum(:hours) || 0
end
-
-private
- # Creates an history for the issue
- #def build_history
- # @history = self.histories.build
- # @history.status = self.status
- # @history.author = self.author
- #end
+
+ def relations
+ (relations_from + relations_to).sort
+ end
+
+ def all_dependent_issues
+ dependencies = []
+ relations_from.each do |relation|
+ dependencies << relation.issue_to
+ dependencies += relation.issue_to.all_dependent_issues
+ end
+ dependencies
+ end
+
+ def duration
+ (start_date && due_date) ? due_date - start_date : 0
+ end
+
+ def soonest_start
+ @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
+ end
end
--- /dev/null
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class IssueRelation < ActiveRecord::Base
+ belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
+ belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
+
+ TYPE_RELATES = "relates"
+ TYPE_DUPLICATES = "duplicates"
+ TYPE_BLOCKS = "blocks"
+ TYPE_PRECEDES = "precedes"
+
+ TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
+ TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 },
+ TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
+ TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
+ }.freeze
+
+ validates_presence_of :issue_from, :issue_to, :relation_type
+ validates_inclusion_of :relation_type, :in => TYPES.keys
+ validates_numericality_of :delay, :allow_nil => true
+ validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
+
+ def validate
+ if issue_from && issue_to
+ errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
+ errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id
+ errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
+ end
+ end
+
+ def other_issue(issue)
+ (self.issue_from_id == issue.id) ? issue_to : issue_from
+ end
+
+ def label_for(issue)
+ TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
+ end
+
+ def before_save
+ if TYPE_PRECEDES == relation_type
+ self.delay ||= 0
+ else
+ self.delay = nil
+ end
+ set_issue_to_dates
+ end
+
+ def set_issue_to_dates
+ soonest_start = self.successor_soonest_start
+ if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
+ issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
+ issue_to.save
+ end
+ end
+
+ def successor_soonest_start
+ return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
+ (issue_from.due_date || issue_from.start_date) + 1 + delay
+ end
+
+ def <=>(relation)
+ TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
+ end
+end
--- /dev/null
+<%= error_messages_for 'relation' %>
+
+<p><%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %>
+<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %>
+<span id="predecessor_fields" style="display:none;">
+<%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %>
+</span>
+<%= submit_tag l(:button_add) %></p>
+
+<%= javascript_tag "setPredecessorFieldsVisibility();" %>
--- /dev/null
+<h3><%=l(:label_related_issues)%></h3>
+
+<table style="width:100%">
+<% @issue.relations.each do |relation| %>
+<tr>
+<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> <%= link_to_issue relation.other_issue(@issue) %></td>
+<td><%=h relation.other_issue(@issue).subject %></td>
+<td><div class="square" style="background:#<%= relation.other_issue(@issue).status.html_color %>;"></div> <%= relation.other_issue(@issue).status.name %></td>
+<td><%= format_date(relation.other_issue(@issue).start_date) %></td>
+<td><%= format_date(relation.other_issue(@issue).due_date) %></td>
+<td><%= link_to_remote image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation},
+ :method => :post
+ }, :title => l(:label_relation_delete) %></td>
+</tr>
+<% end %>
+</table>
+
+<% if authorize_for('issue_relations', 'new') %>
+ <% remote_form_for(:relation, @relation, :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue}, :method => :post) do |f| %>
+ <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
+ <% end %>
+<% end %>
</div>
+<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
+<div id="relations" class="box">
+<%= render :partial => 'relations' %>
+</div>
+<% end %>
+
<div id="history" class="box">
<h3><%=l(:label_history)%>
<% if @journals_count > @journals.length %>(<%= l(:label_last_changes, @journals.length) %>)<% end %></h3>
map.connect 'help/:ctrl/:page', :controller => 'help'\r
#map.connect ':controller/:action/:id/:sort_key/:sort_order'\r
+ map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
+
# Allow downloading Web Service WSDL as a file with an extension
# instead of a file named 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'
--- /dev/null
+class CreateIssueRelations < ActiveRecord::Migration
+ def self.up
+ create_table :issue_relations do |t|
+ t.column :issue_from_id, :integer, :null => false
+ t.column :issue_to_id, :integer, :null => false
+ t.column :relation_type, :string, :default => "", :null => false
+ t.column :delay, :integer
+ end
+ end
+
+ def self.down
+ drop_table :issue_relations
+ end
+end
--- /dev/null
+class AddRelationsPermissions < ActiveRecord::Migration
+ def self.up
+ Permission.create :controller => "issue_relations", :action => "new", :description => "label_relation_new", :sort => 1080, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ Permission.create :controller => "issue_relations", :action => "destroy", :description => "label_relation_delete", :sort => 1085, :is_public => false, :mail_option => 0, :mail_enabled => 0
+ end
+
+ def self.down
+ Permission.find_by_controller_and_action("issue_relations", "new").destroy
+ Permission.find_by_controller_and_action("issue_relations", "destroy").destroy
+ end
+end
activerecord_error_not_a_number: не е число
activerecord_error_not_a_date: е невалидна дата
activerecord_error_greater_than_start_date: трябва да е след началната дата
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d yr
general_fmt_age_plural: %d yrs
field_spent_on: Дата
field_identifier: Идентификатор
field_is_filter: Използва се за филтър
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Заглавие
setting_app_subtitle: Описание
label_related_issues: Свързани задачи
label_applied_status: Промени статуса на
label_loading: Зареждане...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Вход
button_submit: Изпращане
activerecord_error_not_a_number: ist keine Zahl
activerecord_error_not_a_date: ist kein gültiges Datum
activerecord_error_greater_than_start_date: muss größer als Anfangsdatum sein
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d Jahr
general_fmt_age_plural: %d Jahre
field_spent_on: Datum
field_identifier: Identifier
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Applikation Titel
setting_app_subtitle: Applikation Untertitel
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Einloggen
button_submit: OK
activerecord_error_not_a_number: is not a number
activerecord_error_not_a_date: is not a valid date
activerecord_error_greater_than_start_date: must be greater than start date
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d yr
general_fmt_age_plural: %d yrs
field_spent_on: Date
field_identifier: Identifier
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Application title
setting_app_subtitle: Application subtitle
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Login
button_submit: Submit
activerecord_error_not_a_number: is not a number
activerecord_error_not_a_date: no es una fecha válida
activerecord_error_greater_than_start_date: debe ser la fecha mayor que del comienzo
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d año
general_fmt_age_plural: %d años
field_spent_on: Fecha
field_identifier: Identifier
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Título del aplicación
setting_app_subtitle: Subtítulo del aplicación
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Conexión
button_submit: Someter
-_gloc_rule_default: '|n| n<=1 ? "" : "_plural" '
+_gloc_rule_default: '|n| n==1 ? "" : "_plural" '
actionview_datehelper_select_day_prefix:
actionview_datehelper_select_month_names: Janvier,Février,Mars,Avril,Mai,Juin,Juillet,Août,Septembre,Octobre,Novembre,Décembre
activerecord_error_not_a_number: n'est pas un nombre
activerecord_error_not_a_date: n'est pas une date valide
activerecord_error_greater_than_start_date: doit être postérieur à la date de début
+activerecord_error_not_same_project: n'appartient pas au même projet
+activerecord_error_circular_dependency: Cette relation créerait une dépendance circulaire
general_fmt_age: %d an
general_fmt_age_plural: %d ans
field_spent_on: Date
field_identifier: Identifiant
field_is_filter: Utilisé comme filtre
+field_issue_to_id: Demande liée
+field_delay: Retard
setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
label_related_issues: Demandes liées
label_applied_status: Statut appliqué
label_loading: Chargement...
+label_relation_new: Nouvelle relation
+label_relation_delete: Supprimer la relation
+label_relates_to: lié à
+label_duplicates: doublon de
+label_blocks: bloque
+label_blocked_by: bloqué par
+label_precedes: précède
+label_follows: suit
+label_end_to_start: début à fin
+label_end_to_end: fin à fin
+label_start_to_start: début à début
+label_start_to_end: début à fin
button_login: Connexion
button_submit: Soumettre
activerecord_error_not_a_number: non e' un numero
activerecord_error_not_a_date: non e' una data valida
activerecord_error_greater_than_start_date: deve essere maggiore della data di partenza
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d yr
general_fmt_age_plural: %d yrs
field_spent_on: Data
field_identifier: Identifier
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Titolo applicazione
setting_app_subtitle: Sottotitolo applicazione
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Login
button_submit: Invia
activerecord_error_not_a_number: が数字ではありません
activerecord_error_not_a_date: の日付が間違っています
activerecord_error_greater_than_start_date: を開始日より後にしてください
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d歳
general_fmt_age_plural: %d歳
field_spent_on: 日付
field_identifier: 識別子
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: アプリケーションのタイトル
setting_app_subtitle: アプリケーションのサブタイトル
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: ログイン
button_submit: 変更
activerecord_error_not_a_number: nao e um numero
activerecord_error_not_a_date: nao e uma data valida
activerecord_error_greater_than_start_date: deve ser maior que a data inicial
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d yr
general_fmt_age_plural: %d yrs
field_spent_on: Data
field_identifier: Identificador
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: Titulo da aplicacao
setting_app_subtitle: Sub-titulo da aplicacao
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: Login
button_submit: Enviar
activerecord_error_not_a_number: 不是数字
activerecord_error_not_a_date: 不是有效的日期
activerecord_error_greater_than_start_date: 必需大于开始日期
+activerecord_error_not_same_project: doesn't belong to the same project
+activerecord_error_circular_dependency: This relation would create a circular dependency
general_fmt_age: %d yr
general_fmt_age_plural: %d yrs
field_spent_on: 日期
field_identifier: Identifier
field_is_filter: Used as a filter
+field_issue_to_id: Related issue
+field_delay: Delay
setting_app_title: 应用程序标题
setting_app_subtitle: 应用程序子标题
label_related_issues: Related issues
label_applied_status: Applied status
label_loading: Loading...
+label_relation_new: New relation
+label_relation_delete: Delete relation
+label_relates_to: related tp
+label_duplicates: duplicates
+label_blocks: blocks
+label_blocked_by: blocked by
+label_precedes: precedes
+label_follows: follows
+label_end_to_start: start to end
+label_end_to_end: end to end
+label_start_to_start: start to start
+label_start_to_end: start to end
button_login: 登录
button_submit: 提交
return false;
}
+function setPredecessorFieldsVisibility() {
+ relationType = $('relation_relation_type');
+ if (relationType && relationType.value == "precedes") {
+ Element.show('predecessor_fields');
+ } else {
+ Element.hide('predecessor_fields');
+ }
+}
+
/* shows and hides ajax indicator */
Ajax.Responders.register({
onCreate: function(){