Files
redmine/plugins/redmine_ics_export/lib/redmics/export.rb
2023-02-27 19:31:13 +01:00

572 lines
22 KiB
Ruby

# redmics - redmine ics export plugin
# Copyright (c) 2011-2022 Frank Schwarz, frank.schwarz@buschmais.com
#
# 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 Redmics
class Export
include Redmics
include Redmine::I18n
def initialize(controller)
@controller = controller
@priority_count = IssuePriority.all.length
end
def settings(args)
@user = args[:user]
@project = args[:project]
@query = args[:query]
@status = args[:status]
@alarm = args[:alarm]
@assignment = args[:assignment]
@issue_strategy = args[:issue_strategy]
@version_strategy = args[:version_strategy]
@summary_strategy = args[:summary_strategy]
@description_strategy = args[:description_strategy]
end
def icalendar
issues_renderer = create_issues_renderer @issue_strategy
versions_renderer = create_versions_renderer @version_strategy
if @query
(issues, versions) = redmine_query
else
(issues, versions) = redmics_query
end
events = []
events += issues.collect(&issues_renderer).to_a.flatten
events += versions.collect(&versions_renderer).to_a.flatten
cal = Icalendar::Calendar.new
cal.publish
events.each { |event| cal.add_event(event) }
return cal
end
private
def redmine_query
begin
issues = []
versions = []
if @query.valid?
# query: issues
issues = @query.issues(
:include => [:tracker, :assigned_to, :priority, :fixed_version, :author],
) unless @issue_strategy == :none
# query: versions -> skip
end
rescue Exception => e
# we will just deliver an empty ical file instead of showing an error page
@controller.logger.warn('No issues have been selected. ' + e.to_s)
end
return [issues, versions]
end
def redmics_query
begin
case @status
when :open
issue_status_condition = ["#{IssueStatus.table_name}.is_closed = ?", false]
version_status_condition = ["#{Version.table_name}.status <> ?", 'closed']
when :all
issue_status_condition = []
version_status_condition = []
else
raise "Unknown status: '#{@status}'."
end
case @assignment
when :my
raise 'Anonymous user cannot have issues assigned.' if @user.anonymous?
assigned_to_condition = ["assigned_to_id = #{@user.id}"]
when :assigned
assigned_to_condition = ["assigned_to_id is not null"]
when :all
assigned_to_condition = []
else
raise "Unknown assignment: '#{@assignment}.'"
end
@query = IssueQuery.new(:name => "_")
@query.project = @project
@query.filters = nil
issues = []
versions = []
# query: issues
c = QueryConditions.new()
c << issue_status_condition unless issue_status_condition.empty?
c << assigned_to_condition unless assigned_to_condition.empty?
issues = @query.issues(
:include => [:tracker, :assigned_to, :priority, :fixed_version, :author],
:conditions => c.conditions) unless @issue_strategy == :none
# query: versions
c = QueryConditions.new()
c << version_status_condition unless version_status_condition.empty?
versions = @query.versions(
:conditions => c.conditions
) unless @version_strategy == :none
c << ["#{Version.table_name}.sharing = ?", 'system']
versions << @query.versions(
:conditions => c.conditions
) unless @version_strategy == :none
versions.flatten!
rescue Exception => e
# we will just deliver an empty ical file instead of showing an error page
@controller.logger.warn('No issues have been selected. ' + e.to_s)
issues = []
versions = []
end
return [issues, versions]
end
def create_issues_renderer(type)
case type
when :none
lambda { |issue|
[]
}
when :vevent_full_span
lambda { |issue|
result = create_issue_vevent_full_span(issue)
apply_issue_common_properties(issue, result)
apply_issue_event_properties(issue, result)
apply_issue_alarm(issue, result) unless @alarm.nil?
enhance_issue_summary(issue, result)
enhance_issue_description(issue, result)
result
}
when :vevent_end_date
lambda { |issue|
result = create_issue_vevent_end_date(issue)
apply_issue_common_properties(issue, result)
apply_issue_event_properties(issue, result)
apply_issue_alarm(issue, result) unless @alarm.nil?
enhance_issue_summary(issue, result)
enhance_issue_description(issue, result)
result
}
when :vevent_start_and_end_date
lambda { |issue|
result = create_issue_vevent_start_and_end_date(issue)
apply_issue_common_properties(issue, result)
apply_issue_event_properties(issue, result)
apply_issue_alarm(issue, result) unless @alarm.nil?
enhance_issue_summary(issue, result)
enhance_issue_description(issue, result)
result
}
when :vtodo
lambda { |issue|
result = create_issue_vtodo(issue)
apply_issue_common_properties(issue, result)
apply_issue_todo_properties(issue, result)
apply_issue_alarm(issue, result) unless @alarm.nil?
enhance_issue_summary(issue, result)
enhance_issue_description(issue, result)
result
}
end
end
def create_versions_renderer(type)
case type
when :none
lambda { |version|
[]
}
when :vevent_full_span
lambda { |version|
result = create_version_vevent_full_span(version)
apply_version_common_properties(version, result)
apply_version_event_properties(version, result)
enhance_version_description(version, result)
result
}
when :vevent_end_date
lambda { |version|
result = create_version_vevent_end_date(version)
apply_version_common_properties(version, result)
apply_version_event_properties(version, result)
enhance_version_description(version, result)
result
}
when :vevent_start_and_end_date
lambda { |version|
result = create_version_vevent_start_and_end_date(version)
apply_version_common_properties(version, result)
apply_version_event_properties(version, result)
enhance_version_description(version, result)
result
}
when :vtodo
lambda { |version|
result = create_version_vtodo(version)
apply_version_common_properties(version, result)
apply_version_todo_properties(version, result)
enhance_version_description(version, result)
result
}
end
end
def create_issue_vevent_full_span(issue)
start_date, due_date = issue_period(issue)
return [] if start_date.nil? || due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
return [event]
end
def create_issue_vevent_end_date(issue)
due_date = issue_period(issue)[1]
return [] if due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(due_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
return [event]
end
def create_issue_vevent_start_and_end_date(issue)
start_date, due_date = issue_period(issue)
if start_date.nil? && due_date.nil?
return []
elsif start_date == due_date
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(start_date + 1)
event.summary = "<> #{issue.subject}"
event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
return [event]
end
result = []
unless start_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(start_date + 1)
event.summary = "> #{issue.subject}"
event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}:s@#{Setting.host_name}"
result << event
end
unless due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(due_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.summary = "< #{issue.subject}"
event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}:e@#{Setting.host_name}"
result << event
end
return result
end
def create_issue_vtodo(issue)
start_date, due_date = issue_period(issue)
todo = Icalendar::Todo.new
unless start_date.nil?
todo.dtstart = Icalendar::Values::Date.new(start_date)
end
unless due_date.nil?
todo.due = Icalendar::Values::Date.new(due_date)
end
todo.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
return [todo]
end
def apply_issue_common_properties(issue, result)
result.each { |event|
event.summary = "#{issue.subject}" unless event.summary
event.priority = map_priority issue.priority.position
event.created = Icalendar::Values::Date.new(issue.created_on)
event.last_modified = issue.updated_on.to_datetime unless issue.updated_on.nil?
event.description = issue.description unless issue.description.nil?
event.categories = [@controller.l(:label_issue).upcase]
event.contact = Icalendar::Values::Text.new(issue.assigned_to.name,
{"ALTREP" => "mailto:#{issue.assigned_to.mail}"}) unless issue.assigned_to.nil?
event.organizer = Icalendar::Values::CalAddress.new("mailto:#{issue.author.mail}", cn: issue.author.name)
event.url = @controller.url_for(:controller => 'issues', :action => 'show', :id => issue.id)
event.sequence = issue.lock_version
}
end
def apply_issue_alarm(issue, result)
if !result.empty?
alarm_trigger = @alarm # strange but seems to be required
result.last.alarm { |alarm|
alarm.description = "This is an event reminder"
alarm.trigger = alarm_trigger
}
end
end
def apply_issue_event_properties(issue, result)
result.each { |event|
event.status = issue.assigned_to ? "CONFIRMED" : "TENTATIVE" unless issue.closed?
}
end
def apply_issue_todo_properties(issue, result)
result.each { |todo|
if issue.closed?
todo.status = "COMPLETED"
todo.completed = issue.updated_on.to_datetime
todo.percent_complete = 100
elsif issue.assigned_to
todo.status = "IN-PROCESS"
todo.percent_complete = issue.done_ratio ? issue.done_ratio.to_i : 0
else
todo.status = "NEEDS-ACTION"
end
}
end
def create_version_vevent_full_span(version)
start_date, due_date = version_period(version)
return [] if start_date.nil? || due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
return [event]
end
def create_version_vevent_end_date(version)
due_date = version_period(version)[1]
return [] if due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(due_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
return [event]
end
def create_version_vevent_start_and_end_date(version)
start_date, due_date = version_period(version)
if start_date.nil? && due_date.nil?
return []
elsif start_date == due_date
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(start_date + 1)
event.summary = "<#> #{l(:label_version)} #{version.name}"
event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
return [event]
end
result = []
unless start_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(start_date)
event.dtend = Icalendar::Values::Date.new(start_date + 1)
event.summary = ">> #{l(:label_version)} #{version.name}"
event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}:s@#{Setting.host_name}"
result << event
end
unless due_date.nil?
event = Icalendar::Event.new
event.dtstart = Icalendar::Values::Date.new(due_date)
event.dtend = Icalendar::Values::Date.new(due_date + 1)
event.summary = "<< #{l(:label_version)} #{version.name}"
event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}:e@#{Setting.host_name}"
result << event
end
return result
end
def create_version_vtodo(version)
start_date, due_date = version_period(version)
todo = Icalendar::Todo.new
unless start_date.nil?
todo.dtstart = Icalendar::Values::Date.new(start_date)
end
unless due_date.nil?
todo.due = Icalendar::Values::Date.new(due_date)
end
todo.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
return [todo]
end
def apply_version_common_properties(version, result)
result.each { |event|
event.summary = "#{@controller.l(:label_version)} #{version.name}" unless event.summary
event.created = Icalendar::Values::Date.new(version.created_on)
event.last_modified = version.updated_on.to_datetime unless version.updated_on.nil?
event.description = version.description unless version.description.nil?
event.categories = [@controller.l(:label_version).upcase]
event.url = @controller.url_for(:controller => 'versions', :action => 'show', :id => version.id)
days = (version.updated_on.to_i - version.created_on.to_i) / 86400
event.sequence = days
}
end
def apply_version_event_properties(version, result)
result.each { |event|
event.status = "CONFIRMED" unless version.closed?
}
end
def apply_version_todo_properties(version, result)
result.each { |todo|
if version.closed?
todo.status = "COMPLETED"
todo.completed = version.updated_on.to_datetime
todo.percent_complete = 100
else
todo.status = "IN-PROCESS"
todo.percent_complete = version.completed_percent.to_i
end
}
end
def enhance_issue_summary(issue, result)
result.each { |item|
case @summary_strategy
when :plain
# no action
when :status
item.summary = "#{item.summary} (#{issue.status.name})" if issue.status
when :ticket_number_and_status
item.summary = "#{item.summary} (#{issue.status.name})" if issue.status
if /(<|>|<>) (.*)/ =~ item.summary
m = Regexp.last_match
item.summary = "#{m[1]} #{issue.tracker} ##{issue.id}: #{m[2]}"
else
item.summary = "#{issue.tracker} ##{issue.id}: #{item.summary}"
end
else
raise "Unknown summary_strategy: '#{@summary_strategy}'."
end
}
end
def enhance_issue_description(issue, result)
result.each { |item|
case @description_strategy
when :plain
# no action
when :url_and_version
header = []
header << "#{issue.tracker} ##{issue.id}: #{item.url}"
header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
when :full_no_url
header = []
header << "#{issue.tracker} ##{issue.id}"
header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
header << "#{@controller.l(:field_author)}: #{issue.author.name}" if issue.author
header << "#{@controller.l(:field_status)}: #{issue.status.name}" if issue.status
header << "#{@controller.l(:field_priority)}: #{issue.priority}" if issue.priority
header << "#{@controller.l(:field_assigned_to)}: #{issue.assigned_to.name}" if issue.assigned_to
header << "#{@controller.l(:field_category)}: #{issue.category.name}" if issue.category
header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
when :full
header = []
header << "#{issue.tracker} ##{issue.id}: #{item.url}"
header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
header << "#{@controller.l(:field_author)}: #{issue.author.name}" if issue.author
header << "#{@controller.l(:field_status)}: #{issue.status.name}" if issue.status
header << "#{@controller.l(:field_priority)}: #{issue.priority}" if issue.priority
header << "#{@controller.l(:field_assigned_to)}: #{issue.assigned_to.name}" if issue.assigned_to
header << "#{@controller.l(:field_category)}: #{issue.category.name}" if issue.category
header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
else
raise "Unknown description_strategy: '#{@description_strategy}'."
end
}
end
def enhance_version_description(version, result)
result.each { |item|
case @description_strategy
when :plain
# no action
when :url_and_version
header = []
header << "#{@controller.l(:field_url)}: #{item.url}"
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
when :full_no_url
header = []
header << "#{@controller.l(:field_url)}"
header << "#{@controller.l(:field_project)}: #{version.project.name}" if version.project
header << "#{@controller.l(:field_status)}: #{version.status}" if version.status
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
when :full
header = []
header << "#{@controller.l(:field_url)}: #{item.url}"
header << "#{@controller.l(:field_project)}: #{version.project.name}" if version.project
header << "#{@controller.l(:field_status)}: #{version.status}" if version.status
if item.description
item.description = header.join("\n") + "\n\n" + item.description
else
item.description = header.join("\n")
end
else
raise "Unknown description_strategy: '#{@description_strategy}'."
end
}
end
def issue_period(issue)
start_date = issue.start_date || (issue.fixed_version.start_date unless issue.fixed_version.nil?)
due_date = issue.due_date || (issue.fixed_version.due_date unless issue.fixed_version.nil?)
return [start_date, due_date]
end
def version_period(version)
return [version.start_date, version.due_date]
end
# isses_priority goes from 'low' (1), 'normal' (2) to 'immediate' (@priority_count)
# icalendar priority goes from 'urgent' (1) to 'low' (9) (btw. 0 = undefined)
def map_priority(isses_priority)
case isses_priority
when 1; 9
when 2; 5
when 3..@priority_count; 1
else 9
end
end
end
end