def _join_skeleton(user, from_treenode_id, to_treenode_id, project_id, annotation_set): """ Take the IDs of two nodes, each belonging to a different skeleton, and make to_treenode be a child of from_treenode, and join the nodes of the skeleton of to_treenode into the skeleton of from_treenode, and delete the former skeleton of to_treenode. All annotations in annotation_set will be linked to the skeleton of to_treenode.""" if from_treenode_id is None or to_treenode_id is None: raise Exception('Missing arguments to _join_skeleton') response_on_error = '' try: from_treenode_id = int(from_treenode_id) to_treenode_id = int(to_treenode_id) try: from_treenode = Treenode.objects.get(pk=from_treenode_id) except Treenode.DoesNotExist: raise Exception("Could not find a skeleton for treenode #%s" % from_treenode_id) try: to_treenode = Treenode.objects.get(pk=to_treenode_id) except Treenode.DoesNotExist: raise Exception("Could not find a skeleton for treenode #%s" % to_treenode_id) from_skid = from_treenode.skeleton_id from_neuron = _get_neuronname_from_skeletonid( project_id, from_skid ) to_skid = to_treenode.skeleton_id to_neuron = _get_neuronname_from_skeletonid( project_id, to_skid ) # Make sure the user has permissions to edit both neurons can_edit_class_instance_or_fail( user, from_neuron['neuronid'], 'neuron') can_edit_class_instance_or_fail( user, to_neuron['neuronid'], 'neuron') # Check if annotations are valid if not check_annotations_on_join(project_id, user, from_neuron['neuronid'], to_neuron['neuronid'], annotation_set): raise Exception("Annotation distribution is not valid for joining. " \ "Annotations for which you don't have permissions have to be kept!") if from_skid == to_skid: raise Exception('Cannot join treenodes of the same skeleton, this would introduce a loop.') # Reroot to_skid at to_treenode if necessary response_on_error = 'Could not reroot at treenode %s' % to_treenode_id _reroot_skeleton(to_treenode_id, project_id) # The target skeleton is removed and its treenode assumes # the skeleton id of the from-skeleton. response_on_error = 'Could not update Treenode table with new skeleton id for joined treenodes.' Treenode.objects.filter(skeleton=to_skid).update(skeleton=from_skid) response_on_error = 'Could not update TreenodeConnector table.' TreenodeConnector.objects.filter( skeleton=to_skid).update(skeleton=from_skid) # Remove skeleton of to_id (deletes cicic part_of to neuron by cascade, # leaving the parent neuron dangling in the object tree). response_on_error = 'Could not delete skeleton with ID %s.' % to_skid ClassInstance.objects.filter(pk=to_skid).delete() # Remove the 'losing' neuron if it is empty _delete_if_empty(to_neuron['neuronid']) # Update the parent of to_treenode. response_on_error = 'Could not update parent of treenode with ID %s' % to_treenode_id Treenode.objects.filter(id=to_treenode_id).update(parent=from_treenode_id, editor=user) # Update linked annotations of neuron response_on_error = 'Could not update annotations of neuron ' \ 'with ID %s' % from_neuron['neuronid'] _update_neuron_annotations(project_id, user, from_neuron['neuronid'], annotation_set) insert_into_log(project_id, user.id, 'join_skeleton', from_treenode.location, 'Joined skeleton with ID %s (neuron: ' \ '%s) into skeleton with ID %s (neuron: %s, annotations: %s)' % \ (to_skid, to_neuron['neuronname'], from_skid, from_neuron['neuronname'], ', '.join(annotation_set))) except Exception as e: raise Exception(response_on_error + ':' + str(e))
def split_skeleton(request, project_id=None): """ The split is only possible if the neuron is not locked or if it is locked by the current user or if the current user belongs to the group of the user who locked it. Of course, the split is also possible if the current user is a super-user. """ treenode_id = int(request.POST['treenode_id']) treenode = Treenode.objects.get(pk=treenode_id) skeleton_id = treenode.skeleton_id upstream_annotation_set = frozenset([v for k,v in request.POST.iteritems() if k.startswith('upstream_annotation_set[')]) downstream_annotation_set = frozenset([v for k,v in request.POST.iteritems() if k.startswith('downstream_annotation_set[')]) cursor = connection.cursor() # Check if the treenode is root! if not treenode.parent: return HttpResponse(json.dumps({'error': 'Can\'t split at the root node: it doesn\'t have a parent.'})) # Check if annotations are valid if not check_annotations_on_split(project_id, skeleton_id, upstream_annotation_set, downstream_annotation_set): raise Exception("Annotation distribution is not valid for splitting. " \ "One part has to keep the whole set of annotations!") skeleton = ClassInstance.objects.select_related('user').get(pk=skeleton_id) project_id=int(project_id) # retrieve neuron of this skeleton neuron = ClassInstance.objects.get( cici_via_b__relation__relation_name='model_of', cici_via_b__class_instance_a_id=skeleton_id) # Make sure the user has permissions to edit can_edit_class_instance_or_fail(request.user, neuron.id, 'neuron') # retrieve the id, parent_id of all nodes in the skeleton # with minimal ceremony cursor.execute(''' SELECT id, parent_id FROM treenode WHERE skeleton_id=%s ''' % skeleton_id) # no need to sanitize # build the networkx graph from it graph = nx.DiGraph() for row in cursor.fetchall(): graph.add_node( row[0] ) if row[1]: # edge from parent_id to id graph.add_edge( row[1], row[0] ) # find downstream nodes starting from target treenode_id # and generate the list of IDs to change, starting at treenode_id (inclusive) change_list = nx.bfs_tree(graph, treenode_id).nodes() if not change_list: # When splitting an end node, the bfs_tree doesn't return any nodes, # which is surprising, because when the splitted tree has 2 or more nodes # the node at which the split is made is included in the list. change_list.append(treenode_id) # create a new skeleton new_skeleton = ClassInstance() new_skeleton.name = 'Skeleton' new_skeleton.project_id = project_id new_skeleton.user = skeleton.user # The same user that owned the skeleton to split new_skeleton.class_column = Class.objects.get(class_name='skeleton', project_id=project_id) new_skeleton.save() new_skeleton.name = 'Skeleton {0}'.format( new_skeleton.id ) # This could be done with a trigger in the database new_skeleton.save() # Create new neuron new_neuron = ClassInstance() new_neuron.name = 'Neuron' new_neuron.project_id = project_id new_neuron.user = skeleton.user new_neuron.class_column = Class.objects.get(class_name='neuron', project_id=project_id) new_neuron.save() new_neuron.name = 'Neuron %s' % str(new_neuron.id) new_neuron.save() # Assign the skeleton to new neuron cici = ClassInstanceClassInstance() cici.class_instance_a = new_skeleton cici.class_instance_b = new_neuron cici.relation = Relation.objects.get(relation_name='model_of', project_id=project_id) cici.user = skeleton.user # The same user that owned the skeleton to split cici.project_id = project_id cici.save() # update skeleton_id of list in treenode table # This creates a lazy QuerySet that, upon calling update, returns a new QuerySet # that is then executed. It does NOT create an update SQL query for every treenode. tns = Treenode.objects.filter(id__in=change_list).update(skeleton=new_skeleton) # update the skeleton_id value of the treenode_connector table tc = TreenodeConnector.objects.filter( relation__relation_name__endswith = 'synaptic_to', treenode__in=change_list, ).update(skeleton=new_skeleton) # setting new root treenode's parent to null Treenode.objects.filter(id=treenode_id).update(parent=None, editor=request.user) # Update annotations of existing neuron to have only over set _update_neuron_annotations(project_id, request.user, neuron.id, upstream_annotation_set) # Update annotations of under skeleton _annotate_entities(project_id, request.user, [new_neuron.id], downstream_annotation_set) # Log the location of the node at which the split was done insert_into_log( project_id, request.user.id, "split_skeleton", treenode.location, "Split skeleton with ID {0} (neuron: {1})".format( skeleton_id, neuron.name ) ) return HttpResponse(json.dumps({}), mimetype='text/json')