# $SnapHashLicense:
#
# SnapLogic - Open source data services
#
# Copyright (C) 2009, SnapLogic, Inc. All rights reserved.
#
# See http://www.snaplogic.org for more information about
# the SnapLogic project.
#
# This program is free software, distributed under the terms of
# the GNU General Public License Version 2. See the LEGAL file
# at the top of the source tree.
#
# "SnapLogic" is a trademark of SnapLogic, Inc.
#
#
# $
# $Id: resource_diff.py 10295 2009-12-22 23:27:28Z dhiraj $
from __future__ import with_statement
from copy import deepcopy
from snaplogic.common.snap_exceptions import *
from snaplogic.common import component_prop_defs
from snaplogic.common import pipeline_prop_defs
from snaplogic.snapi_base import keys
from snaplogic.cc.prop import ListProp,DictProp
from snaplogic.server import RhResponse
from snaplogic.server import repository
"""
Provides a diff between the 'describe' level values of two resource definitions.
The primary purpose of this module is to serve snapi requests from clients that are requesting a diff
between the version of resource definition they have in a pipeline and the current version of the
resource definition stored in the repository. This diff is returned to the client, along with the
latest 'describe' level values of the resource definition.
When do clients need this call?
Clients need this call when they think that the resource definition of a member of a pipeline is stale
and needs to be refreshed. Since the client should inform the user about what has changed, so they can
make corresponding pipeline adjustments to field linking and param mapping, this diff information is
needed.
The response to the client request is provided by rh_resource_diff.
The diff dictionary that is returned to the client has the following structure:
The nested structure of dictionaries and lists closely reflects the structure of the resdefs
being diff-ed. However, unlike the resdef, values are created in the diff only when there is a difference.
For example if there are two resource defs, older one with the input view:
{'input_views': {'inp_calc': { 'description': 'Input view of csv calc',
'fields': [ ['First_Name', 'string', 'First Name'],
['Last_Name', 'string', 'Last Name'],
['Address', 'string', 'Street Address'],
['City', 'string', 'City'],
['State', 'string', 'State'],
['Work_Phone', 'string', 'Phone, Work']], ....}}}
And the newer resource diff:
{'input_views': {'inp_calc': {'description': 'Input view of csv calc',
'fields': [ ['First_Name', 'string', 'First Name'],
['Last_Name', 'string', 'Last Name'],
['Address', 'string', 'Street Address']], ....}}
Note how the current resdef is misisng a few fields. The diff for this would look as follows:
'dict_diff':
{'input_views':
{'dict_diff':
{'inp_calc':
{'dict_diff':
{'fields':
{'list_diff': [ None,
None,
None,
{'diff_info':
{'removed':
{'message': "Field 'City' has been removed from the view",
'prompt': True
}
}
},
{'diff_info':
{'removed':
{'message': "Field 'State' has been removed from the view",
'prompt': True
}
}
},
{'diff_info':
{'removed':
{'message': "Field 'Work_Phone' has been removed from the view",
'prompt': True
}
}
}
]
}
}
}
}
},
For each dictionary that hosts a difference, a dict_diff element is created. For each list that hosts a difference,
a corresponding list_diff element is created. If a diff message is present anywhere in this hierarchical structure,
then a diff_info entry is created. The value of this entry is a dictionary containing 'added', 'removed' or 'changed'
as possible keys (one or more of these keys can occur). The actual message is present at value. The value also
includes a flag called 'prompt'. By default, this flag is True and indicates that the user needs to be prompted
with the message. False means that the message is not very important and need not be displayed.
"""
CHANGED = 'changed'
ADDED = 'added'
REMOVED = 'removed'
def rh_resource_diff(http_req):
"""
Return the diff of the POST-ed resource definition against the definition stored in the repository.
@param http_req: HTTP request
@type http_req: L{HttpRequest}
@return: RhResponse object with data and code to be returned to client.
@rtype: L{RhResponse}
"""
http_req.make_input_rp()
try:
req = http_req.input.next()
except StopIteration:
return RhResponse(http_req.BAD_REQUEST, "Resource URI missing.")
if len(req) != 2:
return RhResponse(http_req.BAD_REQUEST, "Incorrect number of objects in the list (%s). Expected 2" % len(req))
uri = req[0]
old_dict = req[1]
result = {}
with repository.get_instance() as rep:
retval = rep.read_resources([uri], credentials=(http_req.username, http_req.groups))
if not len(retval[keys.SUCCESS]):
(u, reason) = retval[keys.ERROR].popitem()
return RhResponse(http_req.BAD_REQUEST, "Failed to access resource '%s' - %s" % (u, reason))
(u, d) = retval[keys.SUCCESS].popitem()
new_dict = result[keys.RESDEF] = d[keys.RESDEF]
result[keys.GEN_ID] = d[keys.GEN_ID]
result[keys.GUID] = d[keys.GUID]
result[keys.URI] = uri
_handle_pipeline_notations(old_dict, new_dict)
diff = diff_resources(old_dict, new_dict)
if diff is not None:
result[keys.RESOURCE_DIFF] = diff._to_dict()
else:
result[keys.RESOURCE_DIFF] = None
return RhResponse(http_req.OK, result)
def _handle_pipeline_notations(member_dict, current_dict):
"""
Member resources of a pipeline have some pipeline specific values, interpret and transfer them to latest resource.
@param member_dict: The stale resource definition of a member resource, as retrieved from
within the pipeline.
@type member_dict: dict
@param current_dict: The latest 'describe' level resdef from the repository. This dictionary will
be edited to reflect some of the pipeline specific notations
@type current_dict: dict
"""
# First, transfer the renamed fields map from the member resource to the current dict.
m = member_dict.get(keys.RENAMED_FIELDS_MAP)
if m is not None:
current_dict[keys.RENAMED_FIELDS_MAP] = m
# Revert to the original copy of the output view, for pass-through output views. We need
# to make sure the diff only involves original output fields and the current output fields
# and does not involve any pass-through output fields created within the context of the
# pipeline.
for out in member_dict[keys.OUTPUT_VIEW_PASSTHROUGH]:
member_dict[keys.OUTPUT_VIEWS][out][keys.VIEW_FIELDS] = \
deepcopy(member_dict[keys.OUTPUT_VIEWS][out][keys.ORIGINAL_OUTPUT_FIELDS])
# For the current resource dict, make a copy of the original user defined output view, for pass-through views
# as we usually do, when a resource is added to a pipeline.
for out in current_dict[keys.OUTPUT_VIEW_PASSTHROUGH]:
current_dict[keys.OUTPUT_VIEWS][out][keys.ORIGINAL_OUTPUT_FIELDS] = \
deepcopy(current_dict[keys.OUTPUT_VIEWS][out][keys.VIEW_FIELDS])
current_refs = current_dict.get(keys.RESOURCE_REF)
member_refs = member_dict.get(keys.RESOURCE_REF)
if current_refs and member_refs:
# So we have some refs in the old member resdef and also some refs in the current resdef in repository.
# In that case, we need to transfer keys.OVERRIDDEN_VALUES from the old member resdef to the current
# resdef for the resource refs that are still present in the current resdef.
for ref_name in current_refs:
if (ref_name in member_refs) and (keys.OVERRIDDEN_VALUE in member_refs[ref_name]):
current_refs[ref_name][keys.OVERRIDDEN_VALUE] = member_refs[ref_name][keys.OVERRIDDEN_VALUE]
class DiffElement(object):
"""
Represents the diff between the 'describe' level values of two resource definitions.
Each object of this class represents the diff of one value in the two resdefs. The objects
can be organized into a tree like structure to represent a hierarchy of lists and
dictionaries that can be found in a typical resdef. The 'diff' tree is sparsely populated
with values appearing only when there is a difference in the values.
Each diff element can hold 3 different type of state messages simultaneously. The states can be ADDED,
DELETED or CHANGED. Since this object represents a diff, there is a left hand side and a right hand
side to the diff, this is why one message can be for left side and the second one can be right side.
"""
def __init__(self, data_type=None, label=None, fixed_keys=False):
"""
Initialize DiffElement object.
@param data_type: Type of data diff, if it is dict or list, otherwise None.
@type data_type: str
@param label: Label of the resource property being diffed (if the property has a label).
@type label: str
@param fixed_keys: Set to True, if the diff represents a fixed_keys dictionary.
@type fixed_keys: bool
"""
self.label_str = label
self.default_child = None
self.diff = {}
self.fixed_keys = fixed_keys
if data_type == keys.DICT_DIFF:
self.children = {}
elif data_type == keys.LIST_DIFF:
self.children = []
elif data_type is None:
self.children = None
else:
raise SnapValueError("Diff element cannot be created for data type '%s'" % data_type)
self.data_type = data_type
def _to_str(self, l, indent=0, key=None):
"""
Method recursively walks the structure and creates print friendly string of its contents.
@param l: The list which holds the print friendly strings.
@type l: list
@param indent: The indentation of spaces to be used for the strings created by this call. This number increments
during the recursive dive into the structure.
@type indent: int
@param key: If this struct is inside a list or
"""
ind = " "*indent
if key is not None:
l.append("%s===%s===" % (ind, key))
for (state, d) in self.diff.items():
l.append("%s'%s' State: %s Message: %s Prompt: %s" % (ind, self.label_str, state,
d[keys.DIFF_MESSAGE],
d[keys.DIFF_PROMPT]))
if self.children is not None:
l.append("%sChildren:" % ind)
if hasattr(self.children, "keys"):
indices = self.children.keys()
else:
indices = range(len(self.children))
for i in indices:
if self.children[i] is None:
continue
self.children[i]._to_str(l, indent+4, i)
if self.default_child is not None:
l.append("%sDefault Child:" % ind)
self.default_child._to_str(l, indent+4)
return l
def __str__(self):
"""Return str() value of the diff object."""
l = []
self._to_str(l)
return "\n".join(l)
def _to_dict(self):
"""Convert the diff object to a dictionary and return."""
ret = {}
if len(self.diff) > 0:
ret = {keys.DIFF_INFO : deepcopy(self.diff)}
if self.data_type == keys.DICT_DIFF and len(self.children) > 0:
child_dict = {}
for k in self.children:
d = self.children[k]._to_dict()
if d is not None:
child_dict[k] = self.children[k]._to_dict()
if len(child_dict) > 0:
# Create dict_diff entry only if one of the child diffs is non-None
ret[keys.DICT_DIFF] = child_dict
elif self.data_type == keys.LIST_DIFF and len(self.children) > 0:
child_list = []
child_has_diff = False
for i in self.children:
if i is not None:
i = i._to_dict()
child_has_diff = True
child_list.append(i)
if child_has_diff:
# Create list diff only if one of the child elements has a diff.
ret[keys.LIST_DIFF] = child_list
# Return a dict only if we have some diff to report.
if len(ret) > 0:
return ret
else:
return None
def __getitem__(self, k):
"""
Getitem operator for the diff object.
This method applies only if the diff object represents a Dict diff or a List diff.
@param k: Can key for dictionary or index number for list.
@type k: int or str
"""
if self.data_type == keys.DICT_DIFF:
# If the entry does not already exist, then create it.
if k not in self.children:
if self.fixed_keys:
raise SnapObjNotFoundError("%s is a fixed_keys dict, but no entry was found for key '%s'" %
(self.label_str, k))
self.children[k] = deepcopy(self.default_child)
return self.children[k]
elif self.data_type == keys.LIST_DIFF:
l = k + 1 - len(self.children)
if l > 0:
# The list is not long enough, extend it.
self.children.extend([None for i in range(l)])
if self.children[k] is None:
self.children[k] = deepcopy(self.default_child)
return self.children[k]
else:
raise SnapObjTypeError("'%s' is not a list or dictionary" % self.label_str)
def has_messages(self, expected_messages):
"""
Check if the expected messages were found in the diff element tree.
This method was developed mainly to allow unit testing of the resource diff
code. It allows unit tests to call this method to check if the diff code has
flagged all the appropriate diffs in the diff element tree.
@param expected_messages: A dictionary, where the key is '/' separated list
of keys and indexes leading to the location in the diff element tree,
where the message is located. The last element of the key should be the
state of the diff (ADDED, CHANGED REMOVED). The value in the dictionary
is a 2-tuple containing the diff message and the prompt flag expected.
@type expected_messages: dict
@return: None, if the values in the diff element exactly matched the values
specified in expected_messages. If not, a dictionary is returned, again
keyed by the location of the message (as described above) and the value
is an error message explaining what the mismatch is.
@rtype: dict
"""
stk = []
errors = {}
self._check_messages(stk, expected_messages, errors)
for exp in expected_messages:
errors[exp] = "Not Found : %s" % expected_messages[exp][0]
if len(errors) > 0:
return errors
else:
return None
def _check_messages(self, stk, expected_messages, errors):
"""
Recursive method which walks the diff element tree comparing messages against expected messages.
The above has_messages() method calls this recursive method to figure out if
all the expected messages are present in the diff element.
@param stk: The stack which contains all the keys and index numbers traversed
to reach the current location in the diff element tree.
@type stk: list
@param expected_messages: A dictionary, where the key is '/' separated list
of keys and indexes leading to the location in the diff element tree,
where the message is located. The last element of the key should be the
state of the diff (ADDED, CHANGED REMOVED). The value in the dictionary
is a 2-tuple containing the diff message and the prompt flag expected.
@type expected_messages: dict
@param errors: Dictionary keyed by the location of the message (as described
above) and the value is an error message explaining what the mismatch is.
@type errors: dict
"""
for status in self.diff:
l = "/".join(stk + [status])
if l not in expected_messages:
errors[l] = "Unexpected message - '%s'" % self.diff[status][keys.DIFF_MESSAGE]
elif self.diff[status][keys.DIFF_MESSAGE] != expected_messages[l][0]:
errors[l] = "Expected '%s' != '%s'" % (expected_messages[l][0],
self.diff[status][keys.DIFF_MESSAGE])
del expected_messages[l]
elif self.diff[status][keys.DIFF_PROMPT] != expected_messages[l][1]:
errors[l] = "Expected prompt '%s' != '%s'" % (expected_messages[l][1],
self.diff[status][keys.DIFF_PROMPT])
del expected_messages[l]
else:
# Perfect match, we found this message.
del expected_messages[l]
if self.data_type == keys.DICT_DIFF:
for c in self.children:
self.children[c]._check_messages(stk + [c], expected_messages, errors)
if self.data_type == keys.LIST_DIFF:
for (i, c) in enumerate(self.children):
if c is None:
continue
c._check_messages(stk + [str(i)], expected_messages, errors)
def set_child(self, k, value):
"""
Sets children of a diff element.
@param k: Index number (for list) or key (for dict) of the child.
@type k: int or str
@param value: The child diff value.
@type value: L{DiffElement}
"""
if self.data_type in (keys.DICT_DIFF, keys.LIST_DIFF):
self.children[k] = value
else:
raise SnapObjTypeError("'%s' is not a list or dictionary" % self.label_str)
def add_diff(self, state, mesg=None, prompt=True):
"""
@param state: State of the diff, can be the const values ADDED, REMOVED or CHANGED.
@type state: str
@param mesg: Any message that should be printed with the diff.
@type mesg: str
@param prompt: True if the user should be prompted with the change. False, if the change can be merged in
silently.
@type prompt: bool
"""
if state not in (ADDED, CHANGED, REMOVED):
raise SnapValueError("Diff element cannot have state %s" % state)
if state in self.diff:
raise SnapObjExistsError("A message already exists here ('%s' - '%s')" %
(self.diff[state][keys.DIFF_MESSAGE], mesg))
self.diff[state] = {keys.DIFF_MESSAGE : mesg, keys.DIFF_PROMPT : prompt}
# Now, let us create the DiffElement based tree from the resdef of a member resource.
prop_stack = []
prop_stack.append((None, None, pipeline_prop_defs.MEMBER_RESDEF))
MEMBER_DIFF = None
while len(prop_stack) > 0:
(parent_diff, child_key, prop_def) = prop_stack.pop()
if isinstance(prop_def, DictProp):
diff_obj = DiffElement(keys.DICT_DIFF, prop_def.label_str, prop_def.fixed_keys)
# We assume that a dictionary can behave either as a struct with some predefined
# entries or, as a dictionary of values of a particular type. For now, we don't
# handle the mixed situation of Dict prop having some defined data types
# for some entries and the remaining entries as default type. We do
# this because we don't have such a scenario in any of our internal resdef
# structures and it might even make sense to actually disallow it in DictProp,
# in the future.
if len(prop_def) > 0:
for k in prop_def:
prop_stack.append((diff_obj, k, prop_def[k]))
else:
prop_stack.append((diff_obj, None, prop_def.default_entry_type))
elif isinstance(prop_def, ListProp):
diff_obj = DiffElement(keys.LIST_DIFF, prop_def.label_str)
prop_stack.append((diff_obj, None, prop_def.default_entry_type))
else:
diff_obj = DiffElement(None, prop_def.label_str)
if parent_diff is not None:
if child_key is None:
parent_diff.default_child = diff_obj
else:
parent_diff.set_child(child_key, diff_obj)
else:
MEMBER_DIFF = diff_obj
def create_diff_tree():
return deepcopy(MEMBER_DIFF)
def diff_lists(old_list, new_list):
"""
Does a diff of two lists.
@param old_list: Old state of the list.
@type old_list: list
@param new_list: New state of the list.
@type new_list: list
@return: Returns the result list of all differences, where each entry in the list
is a 3-tuple of (state, index number where the issue is, old value, new value)
@rtype: list
"""
old_len = len(old_list)
new_len = len(new_list)
result_list = []
replacement_entries = []
for (i, old_entry) in enumerate(old_list):
if old_entry not in new_list:
# Could be a delete
if i >= new_len or new_list[i] in old_list:
# The old entry was removed and its place was taken by something that was already in the list,
# or nothing replaced it => A clear case of DELETE.
result_list.append((REMOVED, i, old_entry, None))
else:
# This wasn't a DELETE. It was a CHANGE. Some new entry took its place.
result_list.append((CHANGED, i, old_entry, new_list[i]))
replacement_entries.append(new_list[i])
for (i, new_entry) in enumerate(new_list):
if new_entry in replacement_entries:
# We have already reported on this entry.
continue
if new_entry not in old_list:
# This must have been added.
result_list.append((ADDED, i, None, new_entry))
return result_list
def diff_resources(old_resdict, new_resdict):
"""
Diff this resource def with another resource definition.
@param old_resdict: The older resource definition dictionary.
@type old_resdict: dict
@param new_resdict: The changed/newer resource definition dict.
@type new_resdict: dict
@return: Returns None, if there is no difference, else, returns the diff object.
@rtype: L{DiffElement}
"""
diff = create_diff_tree()
#
# Diff input views first
#
old_views = set(old_resdict[keys.INPUT_VIEWS].keys())
new_views = set(new_resdict[keys.INPUT_VIEWS].keys())
changes = old_views - new_views
for view_name in changes:
diff[keys.INPUT_VIEWS][view_name].add_diff(REMOVED, "Input view '%s' has been removed" % view_name)
changes = new_views - old_views
for view_name in changes:
diff[keys.INPUT_VIEWS][view_name].add_diff(ADDED, "Input view '%s' has been added" % view_name)
common_views = old_views & new_views
for view_name in common_views:
_diff_views(diff[keys.INPUT_VIEWS][view_name], view_name, old_resdict[keys.INPUT_VIEWS][view_name],
new_resdict[keys.INPUT_VIEWS][view_name], True)
#
# Diff output views first
#
old_views = set(old_resdict[keys.OUTPUT_VIEWS].keys())
new_views = set(new_resdict[keys.OUTPUT_VIEWS].keys())
changes = old_views - new_views
for view_name in changes:
diff[keys.OUTPUT_VIEWS][view_name].add_diff(REMOVED, "Output view '%s' has been removed" % view_name)
changes = new_views - old_views
for view_name in changes:
diff[keys.OUTPUT_VIEWS][view_name].add_diff(ADDED, "Output view '%s' has been added" % view_name)
common_views = old_views & new_views
for view_name in common_views:
_diff_views(diff[keys.OUTPUT_VIEWS][view_name], view_name, old_resdict[keys.OUTPUT_VIEWS][view_name],
new_resdict[keys.OUTPUT_VIEWS][view_name], False)
#
# Diff param definitions
#
old_params = set(old_resdict[keys.PARAMS].keys())
new_params = set(new_resdict[keys.PARAMS].keys())
changes = old_params - new_params
for param_name in changes:
diff[keys.PARAMS][param_name].add_diff(REMOVED, "Parameter '%s' has been removed" % param_name)
changes = new_params - old_params
for param_name in changes:
diff[keys.PARAMS][param_name].add_diff(ADDED, "Parameter '%s' has been added" % param_name)
common_params = old_params & new_params
for param_name in common_params:
if old_resdict[keys.PARAMS][param_name][keys.DEFAULT_VALUE] != \
new_resdict[keys.PARAMS][param_name][keys.DEFAULT_VALUE]:
diff[keys.PARAMS][param_name][keys.DEFAULT_VALUE].add_diff(CHANGED,
"Parameter '%s' default value changed from '%s' to '%s'" % (param_name,
old_resdict[keys.PARAMS][param_name][keys.DEFAULT_VALUE],
new_resdict[keys.PARAMS][param_name][keys.DEFAULT_VALUE]))
# NOTE: We don't care about merging the modifiable flag in params dict, ignore it.
#
# Diff pass-through settings.
#
old_pt = set(old_resdict[keys.OUTPUT_VIEW_PASSTHROUGH].keys())
new_pt = set(new_resdict[keys.OUTPUT_VIEW_PASSTHROUGH].keys())
changes = old_pt - new_pt
for out_view in changes:
diff[keys.OUTPUT_VIEW_PASSTHROUGH][out_view].add_diff(REMOVED,
"Output view '%s' has been removed from pass-through settings" % out_view)
changes = new_pt - old_pt
for out_view in changes:
diff[keys.OUTPUT_VIEW_PASSTHROUGH][out_view].add_diff(ADDED,
"Output view '%s' has been added to pass-through settings" % out_view)
common_pt = old_pt & new_pt
for out_view in common_pt:
old_inp_list = old_resdict[keys.OUTPUT_VIEW_PASSTHROUGH][out_view][keys.PT_INPUT_VIEWS]
new_inp_list = new_resdict[keys.OUTPUT_VIEW_PASSTHROUGH][out_view][keys.PT_INPUT_VIEWS]
for (status, i, old_inp, new_inp) in diff_lists(old_inp_list, new_inp_list):
if status == ADDED:
diff[keys.OUTPUT_VIEW_PASSTHROUGH][out_view][keys.PT_INPUT_VIEWS][i].add_diff(ADDED,
"Input view '%s' has been added to pass-through setting of output view '%s'."
% (new_inp, out_view))
elif status == CHANGED:
diff[keys.OUTPUT_VIEW_PASSTHROUGH][out_view][keys.PT_INPUT_VIEWS][i].add_diff(CHANGED,
"Input view '%s' has been replaced by '%s' in pass-through setting of output view '%s'."
% (old_inp, new_inp, out_view))
elif status == REMOVED:
diff[keys.OUTPUT_VIEW_PASSTHROUGH][out_view][keys.PT_INPUT_VIEWS][i].add_diff(REMOVED,
"Input view '%s' has been removed from pass-through setting of output view '%s'."
% (old_inp, out_view))
# NOTE: We don't care about merging modifiable flag in PT settings, ignore it.
#
# Diff categories
#
category_dict = old_resdict.get(keys.RESOURCE_CATEGORY, {})
old_categories = category_dict.get(keys.VALUE, [])
old_modifiable = category_dict.get(keys.MODIFIABLE)
category_dict = new_resdict.get(keys.RESOURCE_CATEGORY, {})
new_categories = category_dict.get(keys.VALUE, [])
new_modifiable = category_dict.get(keys.MODIFIABLE)
if old_modifiable != new_modifiable:
diff[keys.RESOURCE_CATEGORY][keys.MODIFIABLE].add_diff(CHANGED, "Modifiability flag changed from '%s to '%s'" %
(old_modifiable, new_modifiable), False)
for (status, i, old, new) in diff_lists(old_categories, new_categories):
if status == ADDED:
diff[keys.RESOURCE_CATEGORY][keys.VALUE][i].add_diff(ADDED, "Category '%s' has been added" % new)
elif status == CHANGED:
diff[keys.RESOURCE_CATEGORY][keys.VALUE][i].add_diff(CHANGED, "Category '%s' has been changed to '%s'" %
(old, new))
elif status == REMOVED:
diff[keys.RESOURCE_CATEGORY][keys.VALUE][i].add_diff(REMOVED, "Category '%s' has been removed" % old)
#
# Diff resource ref
#
refmap = old_resdict.get(keys.RESOURCE_REF)
if refmap is None:
old_refs = set()
else:
old_refs = set(refmap.keys())
refmap = new_resdict.get(keys.RESOURCE_REF)
if refmap is None:
new_refs = set()
else:
new_refs = set(refmap.keys())
changes = old_refs - new_refs
for ref in changes:
diff[keys.RESOURCE_REF][ref].add_diff(REMOVED, "Resource ref '%s' has been removed" % ref)
changes = new_refs - old_refs
for ref in changes:
diff[keys.RESOURCE_REF][ref].add_diff(ADDED, "Resource ref '%s' has been added" % ref)
common_refs = old_refs & new_refs
for ref_name in common_refs:
old_entry = old_resdict[keys.RESOURCE_REF][ref_name]
new_entry = new_resdict[keys.RESOURCE_REF][ref_name]
if old_entry.get(keys.LABEL) != new_entry.get(keys.LABEL):
diff[keys.RESOURCE_REF][ref_name][keys.LABEL].add_diff(CHANGED, "Resource ref label changed from '%s' to '%s'"
% (old_entry.get(keys.LABEL), new_entry.get(keys.LABEL)), False)
if old_entry.get(keys.REQUIRED) != new_entry.get(keys.REQUIRED):
diff[keys.RESOURCE_REF][ref_name][keys.REQUIRED].add_diff(CHANGED,
"Resource ref required flag changed from '%s' to '%s'"
% (old_entry.get(keys.REQUIRED), new_entry.get(keys.REQUIRED)), False)
if old_entry.get(keys.VALUE) != new_entry.get(keys.VALUE):
diff[keys.RESOURCE_REF][ref_name][keys.VALUE].add_diff(CHANGED, "Resource ref value changed from '%s' to '%s'"
% (old_entry.get(keys.VALUE), new_entry.get(keys.VALUE)))
old_categories = old_entry.get(keys.CATEGORIES_ALLOWED)
if old_categories is None:
old_categories = []
new_categories = new_entry.get(keys.CATEGORIES_ALLOWED)
if new_categories is None:
new_categories = []
for (status, i, old, new) in diff_lists(old_categories, new_categories):
if status == ADDED:
diff[keys.RESOURCE_REF][ref_name][keys.CATEGORIES_ALLOWED][i].add_diff(ADDED,
"Allowed category '%s' has been added" % new)
elif status == CHANGED:
diff[keys.RESOURCE_REF][ref_name][keys.CATEGORIES_ALLOWED][i].add_diff(CHANGED,
"Allowed category '%s' has been changed to '%s'" % (old, new))
elif status == REMOVED:
diff[keys.RESOURCE_REF][ref_name][keys.CATEGORIES_ALLOWED][i].add_diff(REMOVED,
"Allowed category '%s' has been removed" % old)
return diff
def _diff_views(diff, view_name, old_view, new_view, is_input):
"""
Diff the old definition and new definition of views.
@param diff: The diff object that must be populated with the diff.
@type diff: L{DiffElement}
@param view_name: Name of the view being diffed.
@type view_name: str
@param old_view: The old view definition dictionary.
@type old_view: dict
@param new_view: The new view definition dictionary.
@type new_view: dict
@param is_input: A flag indicating whether it is the input view or not.
@type is_input: bool
"""
if old_view[keys.VIEW_IS_RECORD] != new_view[keys.VIEW_IS_RECORD]:
if old_view[keys.VIEW_IS_RECORD] is True:
from_str = "record mode"
to_str = "binary mode"
else:
from_str = "binary mode"
to_str = "record mode"
diff[keys.VIEW_IS_RECORD].add_diff(CHANGED, "View '%s' has been changed from '%s' to '%s'" %
(view_name, from_str, to_str))
# Too drastic a change, no more diffs needed.
return
if old_view[keys.DESCRIPTION] != new_view[keys.DESCRIPTION]:
diff[keys.DESCRIPTION].add_diff(CHANGED, "Description of view '%s' has been changed from '%s' to '%s'" %
(view_name, old_view[keys.DESCRIPTION], new_view[keys.DESCRIPTION]), False)
if old_view[keys.VIEW_IS_RECORD]:
old_fields = old_view[keys.VIEW_FIELDS]
new_fields = new_view[keys.VIEW_FIELDS]
# Check if field names have changed:
old_fdict = {}
old_field_names = []
for f in old_fields:
old_field_names.append(f[keys.FIELD_NAME])
old_fdict[f[keys.FIELD_NAME]] = (f[keys.FIELD_TYPE], f[keys.FIELD_DESC])
new_fdict = {}
new_field_names = []
for f in new_fields:
new_field_names.append(f[keys.FIELD_NAME])
new_fdict[f[keys.FIELD_NAME]] = (f[keys.FIELD_TYPE], f[keys.FIELD_DESC])
for (status, i, old_name, new_name) in diff_lists(old_field_names, new_field_names):
if status == ADDED:
diff[keys.VIEW_FIELDS][i].add_diff(status, "Field '%s' has been added to the view" % new_name)
elif status == CHANGED:
diff[keys.VIEW_FIELDS][i].add_diff(status, "Field '%s' has been replaced with '%s'" %
(old_name, new_name))
elif status == REMOVED:
diff[keys.VIEW_FIELDS][i].add_diff(status, "Field '%s' has been removed from the view" % old_name)
# Next, check for changes in fields that exist in both old and new views.
for (i, f) in enumerate(old_field_names):
if f not in new_field_names:
continue
if old_fdict[f][0] != new_fdict[f][0]:
diff[keys.VIEW_FIELDS][i][keys.FIELD_TYPE].add_diff(CHANGED,
"Type of field '%s' has been changed from '%s' to '%s'" %
(f, old_fdict[f][0], new_fdict[f][0]))
if old_fdict[f][1] != new_fdict[f][1]:
diff[keys.VIEW_FIELDS][i][keys.FIELD_DESC].add_diff(CHANGED,
"Description of field '%s' has been changed from '%s' to '%s'" %
(f, old_fdict[f][1], new_fdict[f][1]), False)
else:
old_ctypes = old_view[keys.VIEW_CONTENT_TYPES]
new_ctypes = new_view[keys.VIEW_CONTENT_TYPES]
for (status, i, old_content, new_content) in diff_lists(old_ctypes, new_ctypes):
if status == ADDED:
diff[keys.VIEW_CONTENT_TYPES][i].add_diff(status, "Content type '%s' has been added to the view" %
new_content)
elif status == CHANGED:
diff[keys.VIEW_CONTENT_TYPES][i].add_diff(status, "Content type '%s' has been replaced with '%s'" %
(old_content, new_content))
elif status == REMOVED:
diff[keys.VIEW_CONTENT_TYPES][i].add_diff(status, "Content type '%s' has been removed from the view"
% old_content)
|