Add Helpdesk issue API include serializer

This commit is contained in:
Jason Thistlethwaite
2026-05-04 09:49:42 -04:00
parent ac284d9dc9
commit fba494dada
7 changed files with 442 additions and 0 deletions
@@ -4,6 +4,21 @@ This RedmineUP helpdesk plugin is maintained as local legacy code for the
installed Redmine 3.4.4 environment. Keep entries focused on local behavior,
rollback archives, and LAN test status.
## 2026-04-25 - Issue API Helpdesk Contact Include
- Purpose: let external semantic indexing keep Redmine issues as the canonical
object while still receiving Helpdesk ticket/contact metadata.
- Touched behavior:
- Added `include=helpdesk` support to the Redmine issue API override.
- Added a Helpdesk issue API serializer for ticket/customer fields.
- Upgrade note: see `docs/redmine_issue_api_helpdesk_include.md` before
updating Redmine core issue API views.
- LAN test result:
- Deployed to `192.168.50.170` with rollback backup
`/home/reddev/redmine-plugin-backups/helpdesk-issue-api-include-20260425T094236Z`.
- Confirmed `/issues/39779.json?include=journals,helpdesk` returns
`helpdesk_ticket.contact` with id, name, company, and email.
## 2026-04-21 - Helpdesk Search Read API And Outbox Coverage
- Purpose: make helpdesk ticket and message identity first-class for external
@@ -0,0 +1,107 @@
api.issue do
api.id @issue.id
api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
api.subject @issue.subject
api.description @issue.description
api.start_date @issue.start_date
api.due_date @issue.due_date
api.done_ratio @issue.done_ratio
api.is_private @issue.is_private
api.estimated_hours @issue.estimated_hours
api.total_estimated_hours @issue.total_estimated_hours
if User.current.allowed_to?(:view_time_entries, @project)
api.spent_hours(@issue.spent_hours)
api.total_spent_hours(@issue.total_spent_hours)
end
render_api_custom_values @issue.visible_custom_field_values, api
api.created_on @issue.created_on
api.updated_on @issue.updated_on
api.closed_on @issue.closed_on
if include_in_api_response?('helpdesk')
helpdesk_ticket = RedmineHelpdesk::IssueApiSerializer.serialize(@issue)
if helpdesk_ticket
api.helpdesk_ticket do
api.id helpdesk_ticket[:id]
api.contact_id helpdesk_ticket[:contact_id]
api.message_id helpdesk_ticket[:message_id]
api.source helpdesk_ticket[:source]
api.is_incoming helpdesk_ticket[:is_incoming]
api.from_address helpdesk_ticket[:from_address]
api.to_address helpdesk_ticket[:to_address]
api.cc_address helpdesk_ticket[:cc_address]
api.ticket_date helpdesk_ticket[:ticket_date]
if helpdesk_ticket[:contact]
api.contact do
api.id helpdesk_ticket[:contact][:id]
api.name helpdesk_ticket[:contact][:name]
api.company helpdesk_ticket[:contact][:company]
api.email helpdesk_ticket[:contact][:email]
end
end
end
else
api.helpdesk_ticket nil
end
end
render_api_issue_children(@issue, api) if include_in_api_response?('children')
api.array :attachments do
@issue.attachments.each do |attachment|
render_api_attachment(attachment, api)
end
end if include_in_api_response?('attachments')
api.array :relations do
@relations.each do |relation|
api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay)
end
end if include_in_api_response?('relations') && @relations.present?
api.array :changesets do
@changesets.each do |changeset|
api.changeset :revision => changeset.revision do
api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
api.comments changeset.comments
api.committed_on changeset.committed_on
end
end
end if include_in_api_response?('changesets')
api.array :journals do
@journals.each do |journal|
api.journal :id => journal.id do
api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
api.notes journal.notes
api.created_on journal.created_on
api.private_notes journal.private_notes
api.array :details do
journal.visible_details.each do |detail|
api.detail :property => detail.property, :name => detail.prop_key do
api.old_value detail.old_value
api.new_value detail.value
end
end
end
end
end
end if include_in_api_response?('journals')
api.array :watchers do
@issue.watcher_users.each do |user|
api.user :id => user.id, :name => user.name
end
end if include_in_api_response?('watchers') && User.current.allowed_to?(:view_issue_watchers, @issue.project)
end
@@ -1,4 +1,5 @@
ActionDispatch::Callbacks.to_prepare do
require 'redmine_helpdesk/issue_api_serializer'
require 'redmine_helpdesk/patches/issues_controller_patch'
require 'redmine_helpdesk/patches/journals_controller_patch'
require 'redmine_helpdesk/patches/attachments_controller_patch'
@@ -0,0 +1,76 @@
require 'time'
module RedmineHelpdesk
module IssueApiSerializer
module_function
def serialize(issue)
ticket = safe_send(issue, :helpdesk_ticket)
return nil unless ticket
contact = safe_send(ticket, :customer)
contact_id = safe_send(ticket, :contact_id) || safe_send(contact, :id)
contact_email = primary_email(contact)
{
:id => safe_send(ticket, :id),
:contact_id => contact_id,
:message_id => safe_send(ticket, :message_id),
:source => safe_send(ticket, :source),
:is_incoming => boolean_send(ticket, :is_incoming?),
:from_address => safe_send(ticket, :from_address),
:to_address => safe_send(ticket, :to_address),
:cc_address => safe_send(ticket, :cc_address),
:ticket_date => iso8601(safe_send(ticket, :ticket_date)),
:contact => contact_payload(contact, contact_id, contact_email)
}
end
def contact_payload(contact, contact_id, contact_email)
return nil unless contact || contact_id || contact_email
payload = {
:id => contact_id,
:name => safe_send(contact, :name),
:company => safe_send(contact, :company),
:email => contact_email
}
reject_blank_values(payload)
end
def primary_email(contact)
email = safe_send(contact, :primary_email)
return email unless blank?(email)
emails = safe_send(contact, :emails)
emails.respond_to?(:first) ? emails.first : nil
end
def iso8601(value)
return nil if blank?(value)
value.respond_to?(:utc) ? value.utc.iso8601 : value.to_s
end
def boolean_send(object, method_name)
return nil unless object && object.respond_to?(method_name)
object.public_send(method_name) ? true : false
end
def safe_send(object, method_name)
return nil unless object && object.respond_to?(method_name)
object.public_send(method_name)
end
def reject_blank_values(payload)
result = {}
payload.each do |key, value|
result[key] = value unless blank?(value)
end
result
end
def blank?(value)
value.nil? || (value.respond_to?(:empty?) && value.empty?)
end
end
end