Skip to content

Commit

Permalink
feat(management command): change merge_attributes args to --save, --d…
Browse files Browse the repository at this point in the history
…elete and --view and update tests and output messages

Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
  • Loading branch information
MyPyDavid committed Jun 7, 2024
1 parent 878d4e9 commit c6a519d
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 42 deletions.
81 changes: 50 additions & 31 deletions rdmo/management/management/commands/merge_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ def add_arguments(self, parser):
help='The URI of the target attribute that will be used to replace the source'
)
parser.add_argument(
'--delete',
'--save',
action='store_true',
default=False,
help='''If specified, the changes will be saved and the source attribute will be deleted.\
help='''If specified, the changes will be saved.\
If not specified, the command will not make any changes in the database.'''
)
parser.add_argument(
'--delete',
action='store_true',
default=False,
help='''If specified, the source attribute will be deleted.\
If not specified, the command will not delete the source attribute.'''
)
parser.add_argument(
'--view',
action='store_true',
Expand All @@ -42,6 +49,7 @@ def add_arguments(self, parser):
def handle(self, *args, **options):
source = options['source']
target = options['target']
save_changes = options['save']
delete_source = options['delete']
update_views = options['view']
verbosity = options.get('verbosity', 1)
Expand All @@ -59,46 +67,55 @@ def handle(self, *args, **options):
for related_field in related_model_fields:
replaced_model_result = replace_attribute_on_related_model_instances(related_field, source=source,
target=target,
delete_source=delete_source)
save_changes=save_changes)
results[related_field.related_model._meta.verbose_name_raw] = replaced_model_result

view_template_result = replace_attribute_in_view_template(source=source, target=target,
delete_source=delete_source,
update_views=update_views)
save_changes=save_changes, update_views=update_views)
results[View._meta.verbose_name_raw] = view_template_result

if delete_source and all(a['saved'] for i in results.values() for a in i):
if delete_source:
try:
source.delete()
logger.info(f"Source attribute {source.uri} was deleted.")
except source.DoesNotExist:
pass
logger.info(f"Source attribute {source.uri} did not exist.")

if verbosity >= 1:

affect_elements_msg = make_elements_message(results)
all_instances_were_updated = all(a['saved'] for i in results.values() for a in i)
affect_elements_msg = make_affected_elements_counts_message(results)
affected_projects_msg = make_affected_projects_message(results)
affected_views_msg = make_affected_views_message(results)
if save_changes:
info_msg = f"Successfully replaced source attribute {source.uri} with {target.uri} on affected elements." # noqa: E501
if update_views:
info_msg += f"\nSuccessfully replaced source attribute {source.uri} in affected View templates."
self.stdout.write(self.style.SUCCESS(info_msg))

if delete_source:
self.stdout.write(self.style.SUCCESS(
f"Successfully replaced source attribute {source.uri} with {target.uri}.\n"
f"Source attribute {source.uri} was deleted.\n"
f"{affect_elements_msg}\n"
f"{affected_projects_msg}\n"
f"{affected_views_msg}"
))
else:
if not all_instances_were_updated:
self.stdout.write(
self.style.NOTICE(f"Source attribute {source.uri} was deleted without moving affected elements to the target attribute.") # noqa: E501
)
if not save_changes and not delete_source:
self.stdout.write(self.style.SUCCESS(
f"Source attribute {source.uri} can be replaced with {target.uri}.\n"
f"{affect_elements_msg}\n"
f"{affected_projects_msg}\n"
f"{affected_views_msg}"
f"Nothing was changed. Displaying affected elements for replacement of source attribute {source.uri} with target {target.uri}." # noqa: E501
))


self.stdout.write(self.style.SUCCESS(
f"{affect_elements_msg}\n"
f"{affected_projects_msg}\n"
f"{affected_views_msg}"
))

if verbosity >= 2:
affected_instances_msg = make_affected_instances_message(results)
if affected_instances_msg:
self.stdout.write()
self.stdout.write(affected_instances_msg)
affected_instances_msg = make_affected_instances_detail_message(results)
self.stdout.write()
self.stdout.write(affected_instances_msg)


def get_valid_attribute(attribute, message_name=''):
Expand All @@ -118,7 +135,7 @@ def get_valid_attribute(attribute, message_name=''):

return attribute

def replace_attribute_on_related_model_instances(related_field, source=None, target=None, delete_source=False):
def replace_attribute_on_related_model_instances(related_field, source=None, target=None, save_changes=False):
model = related_field.related_model
lookup_field = related_field.remote_field.name
qs = model.objects.filter(**{lookup_field: source})
Expand All @@ -132,44 +149,46 @@ def replace_attribute_on_related_model_instances(related_field, source=None, tar

setattr(instance, lookup_field, target)

if delete_source:
if save_changes:
instance.save()
logger.info(f"Attribute {source.uri} on {model._meta.verbose_name_raw}(id={instance.id}) was replaced with {target.uri}.") # noqa: E501

replacement_results.append({
'model_name': model._meta.verbose_name_raw,
'instance': instance,
'source': source,
'target': target,
'saved': delete_source,
'saved': save_changes,
})
return replacement_results


def replace_attribute_in_view_template(source=None, target=None, delete_source=False, update_views=False):
def replace_attribute_in_view_template(source=None, target=None, save_changes=False, update_views=False):
qs = View.objects.filter(**{"template__contains": source.path})
replacement_results = []
for instance in qs:

template_target = replace_uri_in_template_string(instance.template, source, target)
instance.template = template_target

if delete_source and update_views:
if save_changes and update_views:
instance.save()
logger.info(f"Attribute {source.uri} in {View._meta.verbose_name_raw}(id={instance.id}) template was replaced with {target.uri}.") # noqa: E501
logger.info(f"Attribute {source.uri} in {View._meta.verbose_name_raw}(id={instance.id}) template was replaced with target {target.uri}.") # noqa: E501

replacement_results.append({
'model_name': View._meta.verbose_name_raw,
'instance': instance,
'source': source,
'target': target,
'saved': delete_source,
'saved': save_changes,
})
return replacement_results

def get_affected_projects_from_results(results):
value_results = results.get(Value._meta.verbose_name_raw, [])
return list({i['instance'].project for i in value_results})

def make_elements_message(results):
def make_affected_elements_counts_message(results):
element_counts = ", ".join([f"{k.capitalize()}({len(v)})" for k, v in results.items() if v])
if not element_counts or not any(results.values()):
return "No elements affected."
Expand Down Expand Up @@ -198,7 +217,7 @@ def make_affected_views_message(results):
return msg


def make_affected_instances_message(results):
def make_affected_instances_detail_message(results):
if not results:
return ""
msg = ""
Expand Down
59 changes: 48 additions & 11 deletions rdmo/management/tests/test_merge_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ def create_copies_of_attributes_with_new_uri_prefix(example_attributes, new_pref
attribute = _prepare_instance_for_copy(attribute, uri_prefix=new_prefix)
attribute.save()

def get_related_affected_instances(attribute) -> list:

related_qs = []
for related_field in ATTRIBUTE_RELATED_MODELS_FIELDS:
model = related_field.related_model
lookup_field = related_field.remote_field.name
qs = model.objects.filter(**{lookup_field: attribute})
related_qs.append(qs)
return related_qs


@pytest.fixture
def create_merge_attributes(db, settings):
Expand Down Expand Up @@ -119,8 +129,10 @@ def test_that_the_freshly_created_merge_attributes_are_present(create_merge_attr


@pytest.mark.parametrize('source_uri_prefix', new_merge_uri_prefixes)
@pytest.mark.parametrize('save', [False, True])
@pytest.mark.parametrize('delete', [False, True])
def test_command_merge_attributes(create_merge_attributes, source_uri_prefix, delete):
@pytest.mark.parametrize('view', [False, True])
def test_command_merge_attributes(create_merge_attributes, source_uri_prefix, save, delete, view):
source_attributes = Attribute.objects.filter(uri_prefix=source_uri_prefix).all()
assert len(source_attributes) > 2
unique_uri_prefixes = set(Attribute.objects.values_list("uri_prefix", flat=True))
Expand All @@ -130,35 +142,60 @@ def test_command_merge_attributes(create_merge_attributes, source_uri_prefix, de
for source_attribute in source_attributes:
target_attribute = Attribute.objects.get(uri_prefix=EXAMPLE_URI_PREFIX, path=source_attribute.path)
stdout, stderr = io.StringIO(), io.StringIO()
before_source_related_qs = get_related_affected_instances(source_attribute)
before_source_related_view_qs = View.objects.filter(**{"template__contains": source_attribute.path})
# before_target_related_qs = get_related_affected_instances(target_attribute)

command_kwargs = {'source': source_attribute.uri,
'target': target_attribute.uri,
'save': save, 'delete': delete, 'view': view}
failed = False
if source_attribute.get_descendants():
with pytest.raises(CommandError):
call_command('merge_attributes',
source=source_attribute.uri, target=target_attribute.uri, delete=delete,
stdout=stdout, stderr=stderr)
stdout=stdout, stderr=stderr, **command_kwargs)
failed = True
elif target_attribute.get_descendants():
with pytest.raises(CommandError):
call_command('merge_attributes',
source=source_attribute.uri, target=target_attribute.uri, delete=delete,
stdout=stdout, stderr=stderr)
stdout=stdout, stderr=stderr, **command_kwargs)
failed = True
else:
call_command('merge_attributes',
source=source_attribute.uri, target=target_attribute.uri, delete=delete,
stdout=stdout, stderr=stderr)
stdout=stdout, stderr=stderr, **command_kwargs)


if delete and not failed:
# assert that the source attribut was deleted
with pytest.raises(Attribute.DoesNotExist):
Attribute.objects.get(id=source_attribute.id)
else:
assert Attribute.objects.get(id=source_attribute.id)

after_source_related_qs = get_related_affected_instances(source_attribute)
after_source_related_view_qs = View.objects.filter(**{"template__contains": source_attribute.path})
after_target_related_qs = get_related_affected_instances(target_attribute)
# after_target_related_view_qs = View.objects.filter(**{"template__contains": target_attribute.path})

if save and not failed:

if any(i.exists() for i in before_source_related_qs):
assert not any(i.exists() for i in after_source_related_qs)
assert any(i.exists() for i in after_target_related_qs)

if source_attribute.path in merge_view_template_addition_uri_path:
# assert that the attribute in the view template was replaced as well
# the EXAMPLE_VIEW_URI is from the target attribute
# uri_prefix = source_uri_prefix, uri_path = EXAMPLE_VIEW_URI_PATH
for instance in View.objects.filter(**{"template__contains": EXAMPLE_VIEW_URI_PATH}):
assert any(EXAMPLE_VIEW_URI in i for i in instance.template.splitlines())
assert not any(source_attribute.uri in i for i in instance.template.splitlines())
if before_source_related_view_qs.exists():
if view and source_attribute.path != target_attribute.path:
assert not after_source_related_view_qs.exists()
for instance in after_source_related_view_qs:
assert any(target_attribute.path in i for i in instance.template.splitlines())
assert not any(source_attribute.path in i for i in instance.template.splitlines())
else:
assert after_source_related_view_qs.exists()

else:
assert Attribute.objects.get(id=source_attribute.id)
if any(i.exists() for i in before_source_related_qs):
assert any(i.exists() for i in after_source_related_qs)

0 comments on commit c6a519d

Please sign in to comment.