Skip to content

Commit

Permalink
Merge pull request #1289 from dradis/feature/issue-entry-association-…
Browse files Browse the repository at this point in the history
…diff

Feature issue entry association diff
  • Loading branch information
aapomm authored Oct 8, 2024
2 parents 06cd330 + 50eae21 commit d7d67d6
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 33 deletions.
17 changes: 17 additions & 0 deletions app/assets/stylesheets/tylium/modules/_buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
}
}

.btn-link {
background-color: transparent;
border: none;
color: $linkColor;
padding: 0;
text-decoration: none;

&:active,
&:focus,
&:hover,
&:not(:disabled):not(.disabled):active {
background-color: transparent;
box-shadow: none !important;
color: $linkColorHover;
}
}

.btn-placeholder {
align-items: center;
background-color: $primaryColor;
Expand Down
18 changes: 8 additions & 10 deletions app/assets/stylesheets/tylium/modules/_modals.scss
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
.modal {

.modal-dialog {
max-width: 560px;
&:not(.modal-lg, .modal-xl) {
max-width: 560px;
}

.modal-content {
font-size: 0.9rem;

.modal-body {

.add_multiple_nodes_form {
display: none;

.add_multiple_nodes_error {
display: none;
color: #b94a48;
Expand Down Expand Up @@ -49,7 +49,6 @@
}

&#modal_delete_node {

ul {
list-style: disc;
padding-left: 30px;
Expand All @@ -70,7 +69,7 @@
text-decoration: underline !important;
}
}

.invalid-selection {
cursor: not-allowed;
}
Expand Down Expand Up @@ -109,13 +108,12 @@
}

&#try-pro {

.modal-dialog {
max-width: 80%;

.modal-body {
height: 80vh;

iframe {
border: none;
top: 0;
Expand All @@ -125,7 +123,7 @@
width: 100%;
}
}

.modal-header h5 span {
color: $primaryColor;
}
Expand Down
24 changes: 18 additions & 6 deletions app/models/concerns/has_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ def title?
fields["Title"].present?
end

private

def update_container(container_field, updated_fields)
self.send(
:"#{container_field}=",
FieldParser.fields_hash_to_source(updated_fields)
)
end

module ClassMethods
# Method for models to define which attribute is to be converted to fields.
#
Expand Down Expand Up @@ -52,14 +61,17 @@ def dradis_has_fields_for(container_field)
define_method :set_field do |field, value|
# Don't use 'fields' as a local variable name or it conflicts with the
# #fields getter method
updated_fields = fields
updated_fields = fields
updated_fields[field] = value
self.send(
:"#{container_field}=",
updated_fields.to_a.map { |h| "#[#{h[0]}]#\n#{h[1]}" }.join("\n\n")
)
self.update_container(container_field, updated_fields)
end

# Completely removes the field (field header and value) from the content
define_method :delete_field do |field|
updated_fields = fields
updated_fields.except!(field)
self.update_container(container_field, updated_fields)
end
end
end

end
45 changes: 28 additions & 17 deletions app/models/field_parser.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
class FieldParser
FIELDS_REGEX = /#\[(.+?)\]#[\r|\n](.*?)(?=#\[|\z)/m
FIELDLESS_REGEX = /^([\s\S]*?)(?=\n{,2}#\[.+?\]#|\z)/
HEADERLESS_REGEX = /^([\s\S]*?)(?=\n{,2}#\[.+?\]#|\z)/

# Convert serialized form data to Dradis-style item content.
def self.fields_to_source(serialized_form)
serialized_form.each_slice(2).map do |field_name, field_value|
field = field_name[:value]
value = field_value[:value]

str = ''
str << "#[#{field}]#\n" unless field.empty?
str << "#{value}" unless value.empty?
serialized_form.each_slice(2).map(&to_source).compact.join("\n\n")
end

str
end.compact.join("\n\n")
# Convert a hash of field name/value pairs to Dradis-style item content.
def self.fields_hash_to_source(fields)
fields.map(&to_source).compact.join("\n\n")
end

# Parse the contents of the field and split it to return a Hash of field
Expand All @@ -38,7 +34,7 @@ def self.source_to_fields(string)
# #[Description]#
# Lorem ipsum...
#
# If the string contains a fieldless string, it will be prepended to
# If the string contains a headerless field, it will be prepended to
# the result. E.g.
#
# Line 1
Expand All @@ -54,18 +50,33 @@ def self.source_to_fields_array(string)
field.map(&:strip)
end

fieldless_string = parse_fieldless_string(string)
headerless_fields = parse_fields_without_headers(string)

if fieldless_string.present?
array.prepend(['', fieldless_string])
if headerless_fields.present?
array.prepend(['', headerless_fields])
end

array
end

# Field-less strings are strings that do not have a field header (#[Field]#).
# Headerless fields are strings that do not have a field header (#[Field]#).
# This parses all characters before a field header or end of string.
def self.parse_fieldless_string(source)
source[FIELDLESS_REGEX, 1]
def self.parse_fields_without_headers(source)
source[HEADERLESS_REGEX, 1]
end

private

def self.to_source
return Proc.new do |field, value|
field = field.is_a?(String) ? field.to_s : field[:value]
value = value.is_a?(String) ? value.to_s : value[:value]

str = ''
str << "#[#{field}]#\n" unless field.empty?
str << value unless value.empty?

str
end
end
end
120 changes: 120 additions & 0 deletions app/services/diffed_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
class DiffedContent
EXCLUDED_FIELDS = %w(id plugin plugin_id AddonTags)

attr_reader :source, :target

def initialize(source, target)
@source = source
@target = target

normalize_content(@source)
normalize_content(@target)
end

def content_diff
diff_by_word(source.content, target.content)
end

# Lists the fields that have changed between the source and the target and
# saves the diffed content for each field.
#
# Returns a hash with the following structure:
# { field: { source: <diffed_source_field>, target: <diffed_target_field> } }
def unsynced_fields
@unsynced_fields ||=
begin
fields = @source.fields.except(*EXCLUDED_FIELDS).keys |
@target.fields.except(*EXCLUDED_FIELDS).keys

fields.filter_map do |field|
source_value = source.fields[field] || ''
target_value = target.fields[field] || ''

if source_value != target_value
[field, diff_by_word(source_value, target_value)]
end
end.to_h
end
end

def changed?
source.updated_at != target.updated_at &&
source.content != target.content
end

def content_for_update(field_params = nil)
if field_params
{
source: content_with_updated_field_from_target(field: field_params, source: @target.reload, target: @source.reload),
target: content_with_updated_field_from_target(field: field_params, source: @source.reload, target: @target.reload)
}
else
{ source: @target.content, target: @source.content }
end
end

private

# Given a target record, update its field depending on the following cases:
# 1) If the source record has the existing field:
# 1.1) If the target record also has the existing field, update the value
# with the value from the source record
# 1.2) If the target record does not have the field, insert the field
# at the same index where the field is present in the source record
# 2) If the source record is missing the field, delete the field in the
# target record
def content_with_updated_field_from_target(field:, source:, target:)
source_fields = source.fields.keys
source_index = source_fields.excluding(*EXCLUDED_FIELDS).index(field)

# Case 1)
if source_fields.include?(field)
# Case 1.1)
if target.fields.keys.include?(field)
target.set_field(field, source.fields[field])
# Case 1.2)
else
updated_fields = target.fields.to_a.insert(source_index, [field, source.fields[field]])
FieldParser.fields_hash_to_source(updated_fields.compact)
end
# Case 2)
else
target.delete_field(field)
end
end

def diff_by_word(source_content, target_content)
Differ.format = :html
differ_result = Differ.diff_by_word(source_content, target_content)

output = highlighted_string(differ_result)

{ source: output[1], target: output[0] }
end

def normalize_content(record)
fields = record.fields.except(*EXCLUDED_FIELDS)

record.content =
fields.map do |field, value|
"#[#{field}]#\n#{value.gsub("\r",'')}\n"
end.join("\n")
end

def highlighted_string(differ_result)
[:delete, :insert].map do |highlight_type|
result_str = differ_result.dup.to_s

case highlight_type
when :delete
result_str.gsub!(/<del class="differ">(.*?)<\/del>/m, '<mark>\1</mark>')
result_str.gsub!(/<ins class="differ">(.*?)<\/ins>/m, '')
when :insert
result_str.gsub!(/<ins class="differ">(.*?)<\/ins>/m, '<mark>\1</mark>')
result_str.gsub!(/<del class="differ">(.*?)<\/del>/m, '')
end

result_str.html_safe
end
end
end
Loading

0 comments on commit d7d67d6

Please sign in to comment.