# Copyright The IETF Trust 2015-2019, All Rights Reserved # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals import datetime import os import pprint import six import sys import syslog from django.contrib import admin from django.contrib.auth.models import User from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist import debug # pyflakes:ignore from ietf.person.models import Person from ietf.utils.mail import send_mail def merge_persons(source, target, file=sys.stdout, verbose=False): changes = [] # write log syslog.openlog(str(os.path.basename(__file__)), syslog.LOG_PID, syslog.LOG_USER) syslog.syslog("Merging person records {} => {}".format(source.pk,target.pk)) # handle primary emails for email in get_extra_primary(source,target): email.primary = False email.save() changes.append('EMAIL ACTION: {} no longer marked as primary'.format(email.address)) changes.append(handle_users(source,target)) merge_nominees(source, target) move_related_objects(source, target, file=file, verbose=verbose) dedupe_aliases(target) # copy other attributes for field in ('ascii','ascii_short', 'biography', 'photo', 'photo_thumb', 'name_from_draft', 'consent'): if getattr(source,field) and not getattr(target,field): setattr(target,field,getattr(source,field)) target.save() # check for any remaining relationships and exit if more found objs = [source] opts = Person._meta user = User.objects.filter(is_superuser=True).first() admin_site = admin.site using = 'default' deletable_objects = admin.utils.get_deleted_objects( objs, opts, user, admin_site, using) deletable_objects_summary = deletable_objects[1] if len(deletable_objects_summary) > 1: # should only inlcude one object (Person) six.print_("Not Deleting Person: {}({})".format(source.ascii,source.pk), file=file) six.print_("Related objects remain:", file=file) pprint.pprint(deletable_objects[1], stream=file) success = False else: success = True six.print_("Deleting Person: {}({})".format(source.ascii,source.pk), file=file) source.delete() return success, changes def get_extra_primary(source,target): ''' Inspect email addresses and return list of those that should no longer be primary ''' if source.email_set.filter(primary=True) and target.email_set.filter(primary=True): return source.email_set.filter(primary=True) else: return [] def handle_users(source,target,check_only=False): ''' Deactivates extra Users. Retains target user. If check_only == True, just return a string describing action, otherwise perform user changes and return string. ''' if not (source.user or target.user): return "DATATRACKER LOGIN ACTION: none (no login defined)" if not source.user and target.user: return "DATATRACKER LOGIN ACTION: retaining login {}".format(target.user) if source.user and not target.user: message = "DATATRACKER LOGIN ACTION: retaining login {}".format(source.user) if not check_only: target.user = source.user source.user = None source.save() target.save() return message if source.user and target.user: message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user) if not check_only: merge_users(source.user, target.user) syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username)) user = source.user source.user = None source.save() user.is_active = False user.save() return message def move_related_objects(source, target, file, verbose=False): '''Find all related objects and migrate''' related_objects = [ f for f in source._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete ] for related_object in related_objects: accessor = related_object.get_accessor_name() field_name = related_object.field.name queryset = getattr(source, accessor).all() if verbose: six.print_("Merging {}:{}".format(accessor,queryset.count()), file=file) kwargs = { field_name:target } queryset.update(**kwargs) def merge_users(source, target): '''Move related objects from source user to target user''' # handle community list for communitylist in source.communitylist_set.all(): source.communitylist_set.remove(communitylist) target.communitylist_set.add(communitylist) # handle feedback for feedback in source.feedback_set.all(): feedback.user = target feedback.save() # handle nominations for nomination in source.nomination_set.all(): nomination.user = target nomination.save() def dedupe_aliases(person): '''Check person for duplicate aliases and purge''' seen = [] for alias in person.alias_set.all(): if alias.name in seen: alias.delete() else: seen.append(alias.name) def merge_nominees(source, target): '''Move nominees and feedback to target''' for nominee in source.nominee_set.all(): try: target_nominee = target.nominee_set.get(nomcom=nominee.nomcom) except ObjectDoesNotExist: target_nominee = target.nominee_set.create(nomcom=nominee.nomcom, email=target.email()) nominee.nomination_set.all().update(nominee=target_nominee) for fb in nominee.feedback_set.all(): fb.nominees.remove(nominee) fb.nominees.add(target_nominee) for np in nominee.nomineeposition_set.all(): existing_target_np = target_nominee.nomineeposition_set.filter(position=np.position).first() if existing_target_np: if existing_target_np.state.slug=='pending': existing_target_np.state = np.state existing_target_np.save() np.delete() else: np.nominee=target_nominee np.save() nominee.delete() def send_merge_notification(person,changes): ''' Send an email to the merge target (Person) notifying them of the changes ''' send_mail(request = None, to = person.email_address(), frm = "IETF Secretariat ", subject = "IETF Datatracker records merged", template = "utils/merge_person_records.txt", context = dict(person=person,changes='\n'.join(changes)), extra = {} ) def determine_merge_order(source,target): ''' Determine merge order. Select Person that has related User. If both have Users select one with most recent login ''' if source.user and not target.user: source,target = target,source # swap merge order if source.user and target.user: source,target = sorted([source,target],key=lambda a: a.user.last_login if a.user.last_login else datetime.datetime.min) return source,target def get_active_ads(): from ietf.person.models import Person cache_key = "doc:active_ads" active_ads = cache.get(cache_key) if not active_ads: active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type="area").distinct()) cache.set(cache_key, active_ads) return active_ads