# -*- coding: utf-8 -*-
# Akvo Reporting is covered by the GNU Affero General Public License.
# See more details in the license.txt file located at the root folder of the Akvo RSR module.
# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
from typing import Dict, Optional
from django.db import transaction
from akvo.rsr.models import Project
from akvo.rsr.usecases.utils import (
RF_MODELS_CONFIG, get_direct_lineage_hierarchy_ids, make_trees_from_list, make_source_to_target_map
)
from akvo.rsr.models.tree.usecases import check_set_parent, set_parent
[docs]def get_rf_change_candidates(project: Project, new_parent: Project) -> Dict[str, Dict[int, Optional[int]]]:
project_ids = get_direct_lineage_hierarchy_ids(project, new_parent)
if not project_ids:
print('No common ancestor found')
return {}
candidates = {}
for key, config in RF_MODELS_CONFIG.items():
model, parent_attr, project_relation, _ = config
filter_arg = {f"{project_relation}__in": project_ids}
items = model.objects.filter(**filter_arg).values('id', parent_attr, project_relation)
items_tree = make_trees_from_list(items, parent_attr)
candidates[key] = make_source_to_target_map(items_tree, project_relation, project.pk, new_parent.pk)
return candidates
[docs]@transaction.atomic
def change_parent(project: Project, new_parent: Project, reimport=False, verbosity=0):
"""Change the parent of a project to the specified new parent.
This function changes a project's parent including its Result Framework
objects by traversing up the hierarchy to find the nearest common ancestor
then creates a binary lineage tree connecting the project and the new
parent using the nearest common ancestor as root. Then, it uses the lineage
tree connection to resolve each RF object's new parent.
"""
check_set_parent(project, new_parent)
# change parents of RF items
change_candidates = get_rf_change_candidates(project, new_parent)
for key, candidates in change_candidates.items():
model, parent_attr, _, _ = RF_MODELS_CONFIG[key]
for item_id, target_id in candidates.items():
if verbosity > 1:
print(f"Change {key} parent of {item_id} to {target_id}")
model.objects.filter(id__in=[item_id]).update(**{f"{parent_attr}_id": target_id})
if verbosity > 0:
print(f"Change project {project.title} (ID:{project.id}) parent to {new_parent.title} (ID:{new_parent.id})")
# Set the new parent and update the descendants
set_parent(project, new_parent)
if reimport:
if verbosity > 1:
print("Reimporting new parent's results framework")
# Handle any results etc only on the new parent, but not on the old parent.
project.do_import_results(new_parent)
# FIXME: The function could possibly be re-written to make this
# unnecessary? The new ordering is only necessary at the child level if
# parent has no ordering...
ordering = sorted([
# Avoid int vs None comaparison errors
tuple(9999 if it is None else it for it in each)
for each
in project.results.values_list('parent_result__order', 'parent_result__id', 'order', 'id')
])
for order, (_, _, _, result_id) in enumerate(ordering, start=1):
project.results.filter(id=result_id).update(order=order)