def delete_link(request, project_id=None): connector_id = int(request.POST.get('connector_id', 0)) treenode_id = int(request.POST.get('treenode_id', 0)) cursor = connection.cursor() # Make sure the back-end is in the expected state state.validate_state([treenode_id, connector_id], request.POST.get('state'), multinode=True, lock=True, cursor=cursor) links = TreenodeConnector.objects.filter( connector=connector_id, treenode=treenode_id).select_related('relation') if links.count() == 0: raise ValueError('Couldn\'t find link between connector {} ' 'and node {}'.format(connector_id, treenode_id)) link = links[0] # Could be done by filtering above when obtaining the links, # but then one cannot distinguish between the link not existing # and the user_id not matching or not being superuser. can_edit_or_fail(request.user, link.id, 'treenode_connector') deleted_link_id = link.id link.delete() return JsonResponse({ 'link_id': deleted_link_id, 'link_type_id': link.relation.id, 'link_type': link.relation.relation_name, 'result': 'Removed treenode to connector link' })
def delete_connector(request, project_id=None): connector_id = int(request.POST.get("connector_id", 0)) can_edit_or_fail(request.user, connector_id, 'connector') Connector.objects.filter(id=connector_id).delete() return HttpResponse(json.dumps({ 'message': 'Removed connector and class_instances', 'connector_id': connector_id}))
def check_new_annotations(project_id, user, entity_id, annotation_set): """ With respect to annotations, the new annotation set is only valid if the user doesn't remove annotations for which (s)he has no permissions. """ # Get current annotation links annotation_links = ClassInstanceClassInstance.objects.filter( project_id=project_id, class_instance_b__class_column__class_name='annotation', relation__relation_name='annotated_with', class_instance_a_id=entity_id).values_list( 'class_instance_b__name', 'id', 'user') # Build annotation name indexed dict to the link's id and user annotations = {l[0]:(l[1], l[2]) for l in annotation_links} current_annotation_set = frozenset(annotations.keys()) # If the current annotation set is not included completely in the new # set, we have to check if the user has permissions to edit the missing # annotations. removed_annotations = current_annotation_set - annotation_set for rl in removed_annotations: try: can_edit_or_fail(user, annotations[rl][0], 'class_instance_class_instance') except: return False # Otherwise, everything is fine return True
def delete(self, request, project_id, landmarkgroup_id): """Delete one particular landmark group. --- parameters: - name: project_id description: The project the landmark group is part of. type: integer paramType: path required: true - name: landmarkgroup_id description: The ID of the landmark group to delete. required: true type: integer paramType: path """ can_edit_or_fail(request.user, landmarkgroup_id, 'class_instance') landmarkgroup_class = Class.objects.get(project_id=project_id, class_name='landmarkgroup') landmarkgroup = get_object_or_404(ClassInstance, pk=landmarkgroup_id, project_id=project_id, class_column=landmarkgroup_class) landmarkgroup.delete() landmarkgroup.id = None serializer = BasicClassInstanceSerializer(landmarkgroup) return Response(serializer.data)
def remove_annotation(request, project_id=None, annotation_id=None): """ Removes an annotation from one or more entities. """ p = get_object_or_404(Project, pk=project_id) entity_ids = [ int(v) for k, v in request.POST.iteritems() if k.startswith('entity_ids[') ] # Get CICI instance representing the link cici_n_a = ClassInstanceClassInstance.objects.filter( project=p, class_instance_a__id__in=entity_ids, class_instance_b__id=annotation_id) # Make sure the current user has permissions to remove the annotation. missed_cicis = [] cicis_to_delete = [] for cici in cici_n_a: try: can_edit_or_fail(request.user, cici.id, 'class_instance_class_instance') cicis_to_delete.append(cici) except Exception: # Remember links for which permissions are missing missed_cicis.append(cici) # Remove link between entity and annotation for all links on which the user # the necessary permissions has. if cicis_to_delete: ClassInstanceClassInstance.objects \ .filter(id__in=[cici.id for cici in cicis_to_delete]) \ .delete() if len(cicis_to_delete) > 1: message = "Removed annotation from %s entities." % len(cicis_to_delete) elif len(cicis_to_delete) == 1: message = "Removed annotation from one entity." else: message = "No annotation removed." if missed_cicis: message += " Couldn't de-annotate %s entities, due to the lack of " \ "permissions." % len(missed_cicis) # Remove the annotation class instance, regardless of the owner, if there # are no more links to it annotation_links = ClassInstanceClassInstance.objects.filter( project=p, class_instance_b__id=annotation_id) num_annotation_links = annotation_links.count() if num_annotation_links == 0: ClassInstance.objects.get(pk=annotation_id).delete() message += " Also removed annotation instance, because it isn't used " \ "anywhere else." else: message += " There are %s links left to this annotation." \ % num_annotation_links return HttpResponse(json.dumps({'message': message}), content_type='text/json')
def post(self, request: HttpRequest, project_id, sampler_id) -> JsonResponse: """Set fields of a particular sampler. --- parameters: - name: project_id description: The project to operate in. type: integer paramType: path required: false - name: sampler_id description: The sampler to return. type: integer paramType: path required: false - name: leaf_handling_mode description: Optional flag to include all domains of all result sampler results. type: boolean paramType: form required: false """ sampler_id = int(sampler_id) can_edit_or_fail(request.user, sampler_id, 'catmaid_sampler') sampler = Sampler.objects.get(pk=sampler_id) leaf_handling_mode = request.POST.get('leaf_handling_mode') if leaf_handling_mode and leaf_handling_mode in known_leaf_modes: sampler.leaf_segment_handling = leaf_handling_mode sampler.save() return JsonResponse(serialize_sampler(sampler))
def delete_connector(request:HttpRequest, project_id=None) -> JsonResponse: connector_id = int(request.POST.get("connector_id", 0)) can_edit_or_fail(request.user, connector_id, 'connector') # Check provided state cursor = connection.cursor() state.validate_state(connector_id, request.POST.get('state'), node=True, c_links=True, lock=True, cursor=cursor) # Get connector and partner information connectors = list(Connector.objects.filter(id=connector_id).prefetch_related( 'treenodeconnector_set', 'treenodeconnector_set__relation')) if 1 != len(connectors): raise ValueError("Couldn't find exactly one connector with ID #" + str(connector_id)) connector = connectors[0] # TODO: Check how many queries here are generated partners = [{ 'id': p.treenode_id, 'edition_time': p.treenode.edition_time, 'rel': p.relation.relation_name, 'rel_id': p.relation.id, 'confidence': p.confidence, 'link_id': p.id } for p in connector.treenodeconnector_set.all()] connector.delete() return JsonResponse({ 'message': 'Removed connector and class_instances', 'connector_id': connector_id, 'confidence': connector.confidence, 'x': connector.location_x, 'y': connector.location_y, 'z': connector.location_z, 'partners': partners })
def delete_connector(request, project_id=None): connector_id = int(request.POST.get("connector_id", 0)) can_edit_or_fail(request.user, connector_id, 'connector') # Check provided state cursor = connection.cursor() state.validate_state(connector_id, request.POST.get('state'), node=True, c_links=True, lock=True, cursor=cursor) # Get connector and partner information connectors = list(Connector.objects.filter(id=connector_id).prefetch_related( 'treenodeconnector_set', 'treenodeconnector_set__relation')) if 1 != len(connectors): raise ValueError("Couldn't find exactly one connector with ID #" + connector_id) connector = connectors[0] # TODO: Check how many queries here are generated partners = [{ 'id': p.treenode_id, 'edition_time': p.treenode.edition_time, 'rel': p.relation.relation_name, 'rel_id': p.relation.id, 'confidence': p.confidence, 'link_id': p.id } for p in connector.treenodeconnector_set.all()] connector.delete() return JsonResponse({ 'message': 'Removed connector and class_instances', 'connector_id': connector_id, 'confidence': connector.confidence, 'x': connector.location_x, 'y': connector.location_y, 'z': connector.location_z, 'partners': partners })
def delete_sampler(request, project_id, sampler_id): """Delete a sampler if permissions allow it. """ can_edit_or_fail(request.user, sampler_id, "catmaid_sampler") sampler = Sampler.objects.get(id=sampler_id) sampler.delete() return JsonResponse({'deleted_sampler_id': sampler_id})
def delete_connector(request, project_id=None): connector_id = int(request.POST.get("connector_id", 0)) can_edit_or_fail(request.user, connector_id, 'connector') Connector.objects.filter(id=connector_id).delete() return HttpResponse( json.dumps({ 'message': 'Removed connector and class_instances', 'connector_id': connector_id }))
def remove_annotation(request, project_id=None, annotation_id=None): """ Removes an annotation from one or more entities. """ p = get_object_or_404(Project, pk=project_id) entity_ids = [int(v) for k,v in request.POST.iteritems() if k.startswith('entity_ids[')] # Get CICI instance representing the link cici_n_a = ClassInstanceClassInstance.objects.filter(project=p, class_instance_a__id__in=entity_ids, class_instance_b__id=annotation_id) # Make sure the current user has permissions to remove the annotation. missed_cicis = [] cicis_to_delete = [] for cici in cici_n_a: try: can_edit_or_fail(request.user, cici.id, 'class_instance_class_instance') cicis_to_delete.append(cici) except Exception: # Remember links for which permissions are missing missed_cicis.append(cici) # Remove link between entity and annotation for all links on which the user # the necessary permissions has. if cicis_to_delete: ClassInstanceClassInstance.objects \ .filter(id__in=[cici.id for cici in cicis_to_delete]) \ .delete() if len(cicis_to_delete) > 1: message = "Removed annotation from %s entities." % len(cicis_to_delete) elif len(cicis_to_delete) == 1: message = "Removed annotation from one entity." else: message = "No annotation removed." if missed_cicis: message += " Couldn't de-annotate %s entities, due to the lack of " \ "permissions." % len(missed_cicis) # Remove the annotation class instance, regardless of the owner, if there # are no more links to it annotation_links = ClassInstanceClassInstance.objects.filter(project=p, class_instance_b__id=annotation_id) num_annotation_links = annotation_links.count() if num_annotation_links == 0: ClassInstance.objects.get(pk=annotation_id).delete() message += " Also removed annotation instance, because it isn't used " \ "anywhere else." else: message += " There are %s links left to this annotation." \ % num_annotation_links return HttpResponse(json.dumps({'message': message}), content_type='text/json')
def recompute_config(request, project_id, config_id): """Recompute the similarity matrix of the passed in NBLAST configuration. """ can_edit_or_fail(request.user, config_id, 'nblast_config') task = compute_nblast_config.delay(config_id, request.user.id) return JsonResponse({ 'status': 'queued', 'task_id': task.task_id })
def delete_sampler(request, project_id, sampler_id): """Delete a sampler if permissions allow it. """ can_edit_or_fail(request.user, sampler_id, "catmaid_sampler") sampler = Sampler.objects.get(id=sampler_id) sampler.delete() return JsonResponse({ 'deleted_sampler_id': sampler_id })
def delete(self, request: Request, project_id, link_id) -> Response: """Delete a deep-links available to the client. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, id=link_id) can_edit_or_fail(request.user, deep_link.id, 'catmaid_deep_link') deep_link.delete() return Response({'deleted_id': link_id}) except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND)
def recompute_similarity(request, project_id, similarity_id): """Recompute the similarity matrix of the passed in NBLAST configuration. """ simplify = get_request_bool(request.GET, 'simplify', True) required_branches = int(request.GET.get('required_branches', '10')) can_edit_or_fail(request.user, similarity_id, 'nblast_similarity') task = compute_nblast.delay(project_id, request.user.id, similarity_id, remove_target_duplicates=True, simplify=simplify, required_branches=required_branches) return JsonResponse({ 'status': 'queued', 'task_id': task.task_id })
def delete(self, request, project_id, similarity_id): """Delete a NBLAST similarity task. """ can_edit_or_fail(request.user, similarity_id, 'nblast_similarity') similarity = NblastSimilarity.objects.get(pk=similarity_id, project_id=project_id) cursor = connection.cursor() cursor.execute(""" DELETE FROM nblast_similarity WHERE project_id=%s AND id = %s """, [project_id, similarity.id]) return JsonResponse({ 'deleted': True, 'config_id': similarity.id })
def delete(self, request, project_id, config_id): """Delete a NBLAST configuration. """ can_edit_or_fail(request.user, config_id, 'nblast_config') config = NblastConfig.objects.get(pk=config_id, project_id=project_id) cursor = connection.cursor() cursor.execute(""" DELETE FROM nblast_config WHERE project_id=%s AND id = %s """, [project_id, config_id]) return JsonResponse({ 'deleted': True, 'config_id': config_id })
def delete(self, request: HttpRequest, project_id, pointcloud_id) -> JsonResponse: """Delete a point cloud. """ can_edit_or_fail(request.user, pointcloud_id, 'pointcloud') pointcloud = PointCloud.objects.get(pk=pointcloud_id, project_id=project_id) cursor = connection.cursor() cursor.execute( """ DELETE FROM pointcloud CASCADE WHERE project_id=%s AND id = %s """, [project_id, pointcloud_id]) return JsonResponse({'deleted': True, 'pointcloud_id': pointcloud.id})
def delete(self, request, project_id, pointcloud_id): """Delete a point cloud. """ can_edit_or_fail(request.user, pointcloud_id, 'pointcloud') pointcloud = PointCloud.objects.get(pk=pointcloud_id, project_id=project_id) cursor = connection.cursor() cursor.execute(""" DELETE FROM pointcloud CASCADE WHERE project_id=%s AND id = %s """, [project_id, pointcloud_id]) return JsonResponse({ 'deleted': True, 'pointcloud_id': pointcloud.id })
def _remove_annotation(user, project_id, entity_ids, annotation_id): """Remove an annotation made by a certain user in a given project on a set of entities (usually neurons and annotations). Returned is a 4-tuple which holds the deleted annotation links, the list of links that couldn't be deleted due to lack of permission, if the annotation itself was removed (because it wasn't used anymore) and how many uses of this annotation are left. """ p = get_object_or_404(Project, pk=project_id) relations = dict( Relation.objects.filter(project_id=project_id).values_list( 'relation_name', 'id')) # Get CICI instance representing the link cici_n_a = ClassInstanceClassInstance.objects.filter( project=p, relation_id=relations['annotated_with'], class_instance_a__id__in=entity_ids, class_instance_b__id=annotation_id) # Make sure the current user has permissions to remove the annotation. missed_cicis = [] cicis_to_delete = [] for cici in cici_n_a: try: can_edit_or_fail(user, cici.id, 'class_instance_class_instance') cicis_to_delete.append(cici) except Exception: # Remember links for which permissions are missing missed_cicis.append(cici) # Remove link between entity and annotation for all links on which the user # the necessary permissions has. if cicis_to_delete: ClassInstanceClassInstance.objects \ .filter(id__in=[cici.id for cici in cicis_to_delete]) \ .delete() # Remove the annotation class instance, regardless of the owner, if there # are no more links to it annotated_with = Relation.objects.get(project_id=project_id, relation_name='annotated_with') deleted, num_left = delete_annotation_if_unused(project_id, annotation_id, annotated_with) return cicis_to_delete, missed_cicis, deleted, num_left
def delete_link(request, project_id=None): connector_id = int(request.POST.get('connector_id', 0)) treenode_id = int(request.POST.get('treenode_id', 0)) links = TreenodeConnector.objects.filter( connector=connector_id, treenode=treenode_id) if links.count() == 0: return HttpResponse(json.dumps({'error': 'Failed to delete connector #%s from geometry domain.' % connector_id})) # Could be done by filtering above when obtaining the links, # but then one cannot distinguish between the link not existing # and the user_id not matching or not being superuser. can_edit_or_fail(request.user, links[0].id, 'treenode_connector') links[0].delete() return HttpResponse(json.dumps({'result': 'Removed treenode to connector link'}))
def delete(self, request, project_id, landmarkgroup_id, location_id): """Remove the link between a location and a landmark group. --- parameters: - name: project_id description: Project of landmark group type: integer paramType: path required: true - name: landmarkgroup_id description: The landmark group to link type: integer paramType: path required: true - name: location_id description: Existing location ID type: integer paramType: path required: true """ point = Point.objects.get(project_id=project_id, pk=location_id) landmarkgroup = ClassInstance.objects.get( project_id=project_id, pk=landmarkgroup_id, class_column=Class.objects.get(project_id=project_id, class_name="landmarkgroup")) pci = PointClassInstance.objects.get( point=point, user=request.user, class_instance=landmarkgroup, project_id=project_id, relation=Relation.objects.get(project_id=project_id, relation_name="annotated_with")) can_edit_or_fail(request.user, pci.id, 'point_class_instance') pci_id = pci.id pci.delete() return Response({ 'link_id': pci_id, 'point_id': point.id, 'landmarkgroup_id': landmarkgroup.id })
def _remove_annotation(user, project_id, entity_ids, annotation_id): """Remove an annotation made by a certain user in a given project on a set of entities (usually neurons and annotations). Returned is a 4-tuple which holds the deleted annotation links, the list of links that couldn't be deleted due to lack of permission, if the annotation itself was removed (because it wasn't used anymore) and how many uses of this annotation are left. """ p = get_object_or_404(Project, pk=project_id) relations = dict(Relation.objects.filter( project_id=project_id).values_list('relation_name', 'id')) # Get CICI instance representing the link cici_n_a = ClassInstanceClassInstance.objects.filter(project=p, relation_id=relations['annotated_with'], class_instance_a__id__in=entity_ids, class_instance_b__id=annotation_id) # Make sure the current user has permissions to remove the annotation. missed_cicis = [] cicis_to_delete = [] for cici in cici_n_a: try: can_edit_or_fail(user, cici.id, 'class_instance_class_instance') cicis_to_delete.append(cici) except Exception: # Remember links for which permissions are missing missed_cicis.append(cici) # Remove link between entity and annotation for all links on which the user # the necessary permissions has. if cicis_to_delete: ClassInstanceClassInstance.objects \ .filter(id__in=[cici.id for cici in cicis_to_delete]) \ .delete() # Remove the annotation class instance, regardless of the owner, if there # are no more links to it annotated_with = Relation.objects.get(project_id=project_id, relation_name='annotated_with') deleted, num_left = delete_annotation_if_unused(project_id, annotation_id, annotated_with) return cicis_to_delete, missed_cicis, deleted, num_left
def post(self, request, project_id, landmark_id): """Update an existing landmark. Currently, only the name can be updated. --- parameters: - name: project_id description: The project the landmark is part of. type: integer paramType: path required: true - name: landmark_id description: The ID of the landmark. required: true type: integer paramType: path - name: name description: The name of the landmark. required: false type: string paramType: form """ can_edit_or_fail(request.user, landmark_id, 'class_instance') name = request.data.get('name') if not name: raise ValueError('Need name for update') landmark_class = Class.objects.get(project_id=project_id, class_name="landmark") landmark = get_object_or_404(ClassInstance, pk=landmark_id, project_id=project_id, class_column=landmark_class) landmark.name = name landmark.save() landmark.id = None serializer = BasicClassInstanceSerializer(landmark) return Response(serializer.data)
def delete(request, project_id, point_id): """Delete one particular point. --- parameters: - name: project_id description: Project point is part of type: integer paramType: path required: true - name: point_id description: ID of point type: integer paramType: path required: true """ can_edit_or_fail(request.user, point_id, 'point') point = get_object_or_404(Point, pk=point_id, project_id=project_id) point.delete() point.id = None serializer = PointSerializer(point) return Response(serializer.data)
def delete(self, request, project_id, landmark_id, location_id): """Delete the link between a location and a landmark. If the last link to a location is deleted, the location is removed as well. --- parameters: - name: project_id description: Project of landmark group type: integer paramType: path required: true - name: landmark_id description: The landmark to unlink type: integer paramType: path required: true - name: location_id description: The location to unlink paramType: path type: integer required: true """ can_edit_or_fail(request.user, landmark_id, 'class_instance') landmark = ClassInstance.objects.get(project_id=project_id, pk=int(landmark_id)) pci = PointClassInstance.objects.get( project_id=project_id, class_instance=landmark, point_id=int(location_id), relation=Relation.objects.get(project_id=project_id, relation_name='annotated_with')) can_edit_or_fail(request.user, pci.id, 'point_class_instance') pci_id = pci.id pci.delete() deleted_point = False remaining_pci = PointClassInstance.objects.filter( point_id=int(location_id)) if remaining_pci.count() == 0: try: can_edit_or_fail(request.user, point.id, 'point') Point.objects.get(pk=int(location_id)).delete() deleted_point = True except: pass return Response({ 'link_id': pci_id, 'landmark_id': pci.class_instance_id, 'point_id': pci.point_id, 'deleted_point': deleted_point })
def post(self, request, project_id, landmarkgroup_id): """Update an existing landmark group. Currently, only the name and group members can be updated. Edit permissions are only needed when removing group members. --- parameters: - name: project_id description: The project the landmark group is part of. type: integer paramType: path required: true - name: landmark_id description: The ID of the landmark group. required: true type: integer paramType: path - name: name description: The new name of the landmark group. required: false type: string paramType: form - name: members description: The new members of the landmark group. required: false type: array items: type: integer paramType: form """ needs_edit_permissions = False project_id = int(project_id) if not project_id: raise ValueError("Need project ID") landmarkgroup_id = int(landmarkgroup_id) if not landmarkgroup_id: raise ValueError("Need landmark group ID") name = request.data.get('name') if request.data.get('members') == 'none': members = [] else: members = get_request_list(request.data, 'members', map_fn=int) if not name and members == None: raise ValueError('Need name or members parameter for update') landmarkgroup_class = Class.objects.get(project_id=project_id, class_name='landmarkgroup') landmarkgroup = get_object_or_404(ClassInstance, pk=landmarkgroup_id, project_id=project_id, class_column=landmarkgroup_class) if name: landmarkgroup.name = name landmarkgroup.save() if members is not None: # Find out which members need to be added and which existing ones # need to be removed. current_members = set( get_landmark_group_members(project_id, [landmarkgroup_id]).get( landmarkgroup_id, [])) new_members = set(members) to_add = new_members - current_members to_remove = current_members - new_members if to_remove: needs_edit_permissions = True part_of = Relation.objects.get(project_id=project_id, relation_name='part_of') ClassInstanceClassInstance.objects.filter( project_id=project_id, class_instance_a__in=to_remove, class_instance_b_id=landmarkgroup_id, relation=part_of).delete() for landmark_id in to_add: ClassInstanceClassInstance.objects.create( project_id=project_id, class_instance_a_id=landmark_id, class_instance_b_id=landmarkgroup_id, relation=part_of, user=request.user) if needs_edit_permissions: can_edit_or_fail(request.user, landmarkgroup_id, 'class_instance') serializer = BasicClassInstanceSerializer(landmarkgroup) return Response(serializer.data)
def post(self, request, project_id): """Import and link landmarks, landmark groups and locations. The passed in <data> parameter is a list of two-element lists, each representing a group along with its linked landmark and locations. The group is represented by its name and the members are a list of four-element lists, containing the landmark name and the location. This results in the following format: [[group_1_name, [[landmark_1_name, x, y, z], [landmark_2_name, x, y, z]]], ...] Note that this parameter has to be transmitted as a JSON encoded string. --- parameters: - name: project_id description: The project the landmark group is part of. type: integer paramType: path required: true - name: data description: The data to import. required: true type: string paramType: form - name: reuse_existing_groups description: Whether existing groups should be reused. type: boolean paramType: form defaultValue: false required: false - name: reuse_existing_landmarks description: Whether existing landmarks should be reused. type: boolean paramType: form defaultValue: false required: false - name: create_non_existing_groups description: Whether non-existing groups should be created. type: boolean paramType: form defaultValue: true required: false - name: create_non_existing_landmarks description: Whether non-existing landmarks should be created. type: boolean paramType: form defaultValue: true required: false """ project_id = int(project_id) if not project_id: raise ValueError("Need project ID") reuse_existing_groups = request.data.get('reuse_existing_groups', 'false') == 'true' reuse_existing_landmarks = request.data.get('reuse_existing_landmarks', 'false') == 'true' create_non_existing_groups = request.data.get( 'create_non_existing_groups', 'true') == 'true' create_non_existing_landmarks = request.data.get( 'create_non_existing_landmarks', 'true') == 'true' # Make sure the data to import matches our expectations data = request.data.get('data') if not data: raise ValueError("Need data to import") data = json.loads(data) for n, (group_name, landmarks) in enumerate(data): if not group_name: raise ValueError("The {}. group doesn't have a name".format(n)) if not landmarks: raise ValueError( "Group {} doesn't contain any landmarks".format( group_name)) for m, link in enumerate(landmarks): if not link or len(link) != 4: raise ValueError("The {}. link of the {}. group ({}) " \ "doesn't conform to the [ID, X, Y, Z] format.".format(m, n, group_name)) for ci in (1, 2, 3): coordinate = link[ci] value = float(coordinate) if math.isnan(value): raise ValueError("The {}. link of the {}. group ({}) " \ "doesn't have a valid {}. coordinate: {}.".format( m, n, group_name, ci, coordinate)) link[ci] = value classes = get_class_to_id_map(project_id) relations = get_relation_to_id_map(project_id) landmark_class = classes['landmark'] landmarkgroup_class = classes['landmarkgroup'] part_of_relation = relations['part_of'] annotated_with_relation = relations['annotated_with'] landmarks = dict( (k.lower(), v) for k, v in ClassInstance.objects.filter( project_id=project_id, class_column=landmark_class).values_list('name', 'id')) landmarkgroups = dict( (k.lower(), v) for k, v in ClassInstance.objects.filter( project_id=project_id, class_column=landmarkgroup_class).values_list('name', 'id')) imported_groups = [] # Keep track of which landmarks have been seen and were accepted. seen_landmarks = set() for n, (group_name, linked_landmarks) in enumerate(data): # Test if group exists already and raise error if they do and are # prevented from being reused (option). existing_group_id = landmarkgroups.get(group_name.lower()) if existing_group_id: if n == 0: if not reuse_existing_groups: raise ValueError("Group \"{}\" exists already ({}). Please" \ "remove it or enable group re-use.".format( group_name, existing_group_id)) can_edit_or_fail(request.user, existing_group_id, 'class_instance') elif create_non_existing_groups: group = ClassInstance.objects.create( project_id=project_id, class_column_id=landmarkgroup_class, user=request.user, name=group_name) existing_group_id = group.id landmarkgroups[group_name.lower()] = group.id else: raise ValueError("Group \"{}\" does not exist. Please create " \ "it or enable automatic creation/".format(group_name)) imported_landmarks = [] imported_group = { 'id': existing_group_id, 'name': group_name, 'members': imported_landmarks } imported_groups.append(imported_group) for m, link in enumerate(linked_landmarks): landmark_name = link[0] x, y, z = link[1], link[2], link[3] existing_landmark_id = landmarks.get(landmark_name.lower()) if existing_landmark_id: # Test only on first look at landmark if existing_landmark_id not in seen_landmarks: if not reuse_existing_landmarks: raise ValueError("Landmark \"{}\" exists already. " \ "Please remove it or enable re-use of " \ "existing landmarks.".format(landmark_name)) can_edit_or_fail(request.user, existing_landmark_id, 'class_instance') elif create_non_existing_landmarks: landmark = ClassInstance.objects.create( project_id=project_id, class_column_id=landmark_class, user=request.user, name=landmark_name) existing_landmark_id = landmark.id landmarks[landmark_name.lower()] = landmark.id else: raise ValueError("Landmark \"{}\" does not exist. Please " \ "create it or enable automatic creation.".format( landmark_name)) seen_landmarks.add(existing_landmark_id) # Make sure the landmark is linked to the group landmark_link = ClassInstanceClassInstance.objects.get_or_create( project_id=project_id, relation_id=part_of_relation, class_instance_a_id=existing_landmark_id, class_instance_b_id=existing_group_id, defaults={'user': request.user}) # With an existing group and landmark in place, the location can # be linked (to both). point = Point.objects.create(project_id=project_id, user=request.user, editor=request.user, location_x=x, location_y=y, location_z=z) point_landmark = PointClassInstance.objects.create( point=point, user=request.user, class_instance_id=existing_landmark_id, project_id=project_id, relation_id=annotated_with_relation) point_landmark_group = PointClassInstance.objects.create( point=point, user=request.user, class_instance_id=existing_group_id, project_id=project_id, relation_id=annotated_with_relation) imported_landmarks.append({ 'id': existing_landmark_id, 'name': landmark_name, 'x': x, 'y': y, 'z': z }) return Response(imported_groups)
def delete_treenode(request, project_id=None): """ Deletes a treenode. If the skeleton has a single node, deletes the skeleton and its neuron. Returns the parent_id, if any.""" treenode_id = int(request.POST.get('treenode_id', -1)) # Raise an exception if the user doesn't have permission to edit the # treenode. can_edit_or_fail(request.user, treenode_id, 'treenode') # Raise an Exception if the user doesn't have permission to edit the neuron # the skeleton of the treenode is modeling. can_edit_treenode_or_fail(request.user, project_id, treenode_id) # Make sure the back-end is in the expected state state.validate_state(treenode_id, request.POST.get('state'), lock=True, neighborhood=True) treenode = Treenode.objects.get(pk=treenode_id) parent_id = treenode.parent_id # Get information about linked connectors links = list( TreenodeConnector.objects.filter(project_id=project_id, treenode_id=treenode_id).values_list( 'id', 'relation_id', 'connector_id', 'confidence')) n_sampler_refs = SamplerInterval.objects.filter(start_node=treenode).count() + \ SamplerInterval.objects.filter(end_node=treenode).count() if (n_sampler_refs > 0): raise ValueError( "Can't delete node, it is used in at least one sampler interval") response_on_error = '' deleted_neuron = False try: if not parent_id: children = [] # This treenode is root. response_on_error = 'Could not retrieve children for ' \ 'treenode #%s' % treenode_id n_children = Treenode.objects.filter(parent=treenode).count() response_on_error = "Could not delete root node" if n_children > 0: # TODO yes you can, the new root is the first of the children, # and other children become independent skeletons raise Exception("You can't delete the root node when it " "has children.") # Get the neuron before the skeleton is deleted. It can't be # accessed otherwise anymore. neuron = ClassInstance.objects.get( project_id=project_id, cici_via_b__relation__relation_name='model_of', cici_via_b__class_instance_a=treenode.skeleton) # Remove the original skeleton. It is OK to remove it if it only had # one node, even if the skeleton's user does not match or the user # is not superuser. Delete the skeleton, which triggers deleting # the ClassInstanceClassInstance relationship with neuron_id response_on_error = 'Could not delete skeleton.' # Extra check for errors, like having two root nodes count = Treenode.objects.filter(skeleton_id=treenode.skeleton_id) \ .count() if 1 == count: # deletes as well treenodes that refer to the skeleton ClassInstance.objects.filter(pk=treenode.skeleton_id) \ .delete() else: return JsonResponse({"error": "Can't delete " \ "isolated node: erroneously, its skeleton contains more " \ "than one treenode! Check for multiple root nodes."}) # If the neuron modeled by the skeleton of the treenode is empty, # delete it. response_on_error = 'Could not delete neuron #%s' % neuron.id deleted_neuron = _delete_if_empty(neuron.id) if deleted_neuron: # Insert log entry for neuron deletion insert_into_log( project_id, request.user.id, 'remove_neuron', (treenode.location_x, treenode.location_y, treenode.location_z), 'Deleted neuron %s and skeleton(s) %s.' % (neuron.id, treenode.skeleton_id)) else: # Treenode is not root, it has a parent and perhaps children. # Reconnect all the children to the parent. response_on_error = 'Could not update parent id of children nodes' cursor = connection.cursor() cursor.execute( """ UPDATE treenode SET parent_id = %s WHERE project_id = %s AND parent_id = %s RETURNING id, edition_time """, (treenode.parent_id, project_id, treenode.id)) # Children will be a list of two-element lists, just what we want to # return as child info. children = cursor.fetchall() # Remove treenode response_on_error = 'Could not delete treenode.' Treenode.objects.filter(project_id=project_id, pk=treenode_id).delete() return JsonResponse({ 'x': treenode.location_x, 'y': treenode.location_y, 'z': treenode.location_z, 'parent_id': parent_id, 'children': children, 'links': links, 'radius': treenode.radius, 'confidence': treenode.confidence, 'skeleton_id': treenode.skeleton_id, 'deleted_neuron': deleted_neuron, 'success': "Removed treenode successfully." }) except Exception as e: raise Exception(response_on_error + ': ' + str(e))
def post(request, project_id, point_id): """Update one particular point. Requires at least one field to change. --- parameters: - name: project_id description: Project point is part of type: integer paramType: path required: true - name: point_id description: ID of point type: integer paramType: path required: true - name: location_x description: X coordinate type: float paramType: form required: false - name: location_y description: Y coordinate type: float paramType: form required: false - name: location_z description: Z coordinate type: float paramType: form required: false - name: radius description: Optional radius type: float paramType: form required: false - name: confidence description: Optional confidence in [0,5] type: integer paramType: form required: false """ can_edit_or_fail(request.user, point_id, 'point') updated_fields = {} if request.POST.has('x'): updated_fields['location_x'] = float(request.POST.get('x')) if request.POST.has('y'): updated_fields['location_y'] = float(request.POST.get('y')) if request.POST.has('z'): updated_fields['location_z'] = float(request.POST.get('z')) if request.POST.has('radius'): updated_fields['radius'] = float(request.POST.get('radius')) if request.POST.has('confidence'): confidence = max(min(int(request.POST.get('confidence')), 5), 0) updated_fields('confidence', confidence) if not updated_fields: raise ValueError('No field to modify provided') point = get_object_or_404(Point, pk=point_id, project_id=project_id) point.update(**updated_fields) point.save() serializer = PointSerializer(point) return Response(serializer.data)
def delete_treenode(request, project_id=None): """ Deletes a treenode. If the skeleton has a single node, deletes the skeleton and its neuron. Returns the parent_id, if any.""" treenode_id = int(request.POST.get('treenode_id', -1)) # Raise an exception if the user doesn't have permission to edit the # treenode. can_edit_or_fail(request.user, treenode_id, 'treenode') # Raise an Exception if the user doesn't have permission to edit the neuron # the skeleton of the treenode is modeling. can_edit_treenode_or_fail(request.user, project_id, treenode_id) treenode = Treenode.objects.get(pk=treenode_id) parent_id = treenode.parent_id response_on_error = '' deleted_neuron = False try: if not parent_id: # This treenode is root. response_on_error = 'Could not retrieve children for ' \ 'treenode #%s' % treenode_id n_children = Treenode.objects.filter(parent=treenode).count() response_on_error = "Could not delete root node" if n_children > 0: # TODO yes you can, the new root is the first of the children, # and other children become independent skeletons raise Exception("You can't delete the root node when it " "has children.") # Get the neuron before the skeleton is deleted. It can't be # accessed otherwise anymore. neuron = ClassInstance.objects.get(project_id=project_id, cici_via_b__relation__relation_name='model_of', cici_via_b__class_instance_a=treenode.skeleton) # Remove the original skeleton. It is OK to remove it if it only had # one node, even if the skeleton's user does not match or the user # is not superuser. Delete the skeleton, which triggers deleting # the ClassInstanceClassInstance relationship with neuron_id response_on_error = 'Could not delete skeleton.' # Extra check for errors, like having two root nodes count = Treenode.objects.filter(skeleton_id=treenode.skeleton_id) \ .count() if 1 == count: # deletes as well treenodes that refer to the skeleton ClassInstance.objects.filter(pk=treenode.skeleton_id) \ .delete() else: return HttpResponse(json.dumps({"error": "Can't delete " \ "isolated node: erroneously, its skeleton contains more " \ "than one treenode! Check for multiple root nodes."})) # If the neuron modeled by the skeleton of the treenode is empty, # delete it. response_on_error = 'Could not delete neuron #%s' % neuron.id deleted_neuron = _delete_if_empty(neuron.id) else: # Treenode is not root, it has a parent and perhaps children. # Reconnect all the children to the parent. response_on_error = 'Could not update parent id of children nodes' Treenode.objects.filter(parent=treenode) \ .update(parent=treenode.parent) # Remove treenode response_on_error = 'Could not delete treenode.' Treenode.objects.filter(pk=treenode_id).delete() return HttpResponse(json.dumps({ 'deleted_neuron': deleted_neuron, 'parent_id': parent_id, 'skeleton_id': treenode.skeleton_id, 'success': "Removed treenode successfully." })) except Exception as e: raise Exception(response_on_error + ': ' + str(e))
def delete_treenode(request, project_id=None): """ Deletes a treenode. If the skeleton has a single node, deletes the skeleton and its neuron. Returns the parent_id, if any.""" treenode_id = int(request.POST.get('treenode_id', -1)) # Raise an exception if the user doesn't have permission to edit the # treenode. can_edit_or_fail(request.user, treenode_id, 'treenode') # Raise an Exception if the user doesn't have permission to edit the neuron # the skeleton of the treenode is modeling. can_edit_treenode_or_fail(request.user, project_id, treenode_id) # Make sure the back-end is in the expected state state.validate_state(treenode_id, request.POST.get('state'), lock=True, neighborhood=True) treenode = Treenode.objects.get(pk=treenode_id) parent_id = treenode.parent_id # Get information about linked connectors links = list(TreenodeConnector.objects.filter(project_id=project_id, treenode_id=treenode_id).values_list('id', 'relation_id', 'connector_id', 'confidence')) n_sampler_refs = SamplerInterval.objects.filter(start_node=treenode).count() + \ SamplerInterval.objects.filter(end_node=treenode).count() if (n_sampler_refs > 0): raise ValueError("Can't delete node, it is used in at least one sampler interval") response_on_error = '' deleted_neuron = False cursor = connection.cursor() try: if not parent_id: children = [] # This treenode is root. response_on_error = 'Could not retrieve children for ' \ 'treenode #%s' % treenode_id n_children = Treenode.objects.filter(parent=treenode).count() response_on_error = "Could not delete root node" if n_children > 0: # TODO yes you can, the new root is the first of the children, # and other children become independent skeletons raise Exception("You can't delete the root node when it " "has children.") # Get the neuron before the skeleton is deleted. It can't be # accessed otherwise anymore. neuron = ClassInstance.objects.get(project_id=project_id, cici_via_b__relation__relation_name='model_of', cici_via_b__class_instance_a=treenode.skeleton) # Remove the original skeleton. It is OK to remove it if it only had # one node, even if the skeleton's user does not match or the user # is not superuser. Delete the skeleton, which triggers deleting # the ClassInstanceClassInstance relationship with neuron_id response_on_error = 'Could not delete skeleton.' # Extra check for errors, like having two root nodes count = Treenode.objects.filter(skeleton_id=treenode.skeleton_id) \ .count() if 1 == count: # deletes as well treenodes that refer to the skeleton ClassInstance.objects.filter(pk=treenode.skeleton_id) \ .delete() else: return JsonResponse({"error": "Can't delete " \ "isolated node: erroneously, its skeleton contains more " \ "than one treenode! Check for multiple root nodes."}) # If the neuron modeled by the skeleton of the treenode is empty, # delete it. response_on_error = 'Could not delete neuron #%s' % neuron.id deleted_neuron = _delete_if_empty(neuron.id) if deleted_neuron: # Insert log entry for neuron deletion insert_into_log(project_id, request.user.id, 'remove_neuron', (treenode.location_x, treenode.location_y, treenode.location_z), 'Deleted neuron %s and skeleton(s) %s.' % (neuron.id, treenode.skeleton_id)) else: # Treenode is not root, it has a parent and perhaps children. # Reconnect all the children to the parent. response_on_error = 'Could not update parent id of children nodes' cursor.execute(""" UPDATE treenode SET parent_id = %s WHERE project_id = %s AND parent_id = %s RETURNING id, edition_time """, (treenode.parent_id, project_id, treenode.id)) # Children will be a list of two-element lists, just what we want to # return as child info. children = cursor.fetchall() # Remove treenode. Set the current user name in a transaction local # variable. This is done to communicate the current user to the trigger # that updates the skeleton summary table. response_on_error = 'Could not delete treenode.' cursor.execute("SET LOCAL catmaid.user_id=%(user_id)s", { 'user_id': request.user.id, }) Treenode.objects.filter(project_id=project_id, pk=treenode_id).delete() return JsonResponse({ 'x': treenode.location_x, 'y': treenode.location_y, 'z': treenode.location_z, 'parent_id': parent_id, 'children': children, 'links': links, 'radius': treenode.radius, 'confidence': treenode.confidence, 'skeleton_id': treenode.skeleton_id, 'deleted_neuron': deleted_neuron, 'success': "Removed treenode successfully." }) except Exception as e: raise Exception(response_on_error + ': ' + str(e))
def label_update(request, project_id=None, location_id=None, ntype=None): """ location_id is the ID of a treenode or connector. ntype is either 'treenode' or 'connector'. """ labeled_as_relation = Relation.objects.get(project=project_id, relation_name='labeled_as') p = get_object_or_404(Project, pk=project_id) # TODO will FAIL when a tag contains a coma by itself new_tags = request.POST['tags'].split(',') delete_existing_labels = request.POST.get('delete_existing', 'true') == 'true' kwargs = { 'relation': labeled_as_relation, 'class_instance__class_column__class_name': 'label' } table = get_link_model(ntype) if 'treenode' == ntype: kwargs['treenode__id'] = location_id node = Treenode.objects.get(id=location_id) elif 'connector' == ntype: kwargs['connector__id'] = location_id node = Connector.objects.get(id=location_id) if not table: raise Http404('Unknown node type: "%s"' % (ntype, )) # Get the existing list of tags for the tree node/connector and delete any # that are not in the new list. existing_labels = table.objects.filter( **kwargs).select_related('class_instance__name') existing_names = set(ele.class_instance.name for ele in existing_labels) duplicate_labels = table.objects.filter(**kwargs).exclude( class_instance__name__in=new_tags).select_related( 'class_instance__name') other_labels = [] deleted_labels = [] if delete_existing_labels: # Iterate over all labels that should get deleted to check permission # on each one. Remember each label that couldn't be deleted in the # other_labels array. for l in duplicate_labels: try: can_edit_or_fail(request.user, l.id, table._meta.db_table) if remove_label(l.id, ntype): deleted_labels.append(l) else: other_labels.append(l) except: other_labels.append(l) # Create change requests for labels associated to the treenode by other users for label in other_labels: change_request_params = { 'type': 'Remove Tag', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'description': "Remove tag '%s'" % label.class_instance.name, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(label.id), ntype), 'approve_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(label.id), ntype) } ChangeRequest(**change_request_params).save() # Add any new labels. label_class = Class.objects.get(project=project_id, class_name='label') kwargs = { 'user': request.user, 'project': p, 'relation': labeled_as_relation, ntype: node } new_labels = [] for tag_name in new_tags: if len(tag_name) > 0 and tag_name not in existing_names: # Make sure the tag instance exists existing_tags = tuple( ClassInstance.objects.filter(project=p, name=tag_name, class_column=label_class)) if len(existing_tags) < 1: tag = ClassInstance(project=p, name=tag_name, user=request.user, class_column=label_class) tag.save() else: tag = existing_tags[0] # Associate the tag with the treenode/connector. kwargs['class_instance'] = tag tci = table( **kwargs ) # creates new TreenodeClassInstance or ConnectorClassInstance tci.save() new_labels.append(tag_name) if node.user != request.user: # Inform the owner of the node that the tag was added and give them the option of removing it. change_request_params = { 'type': 'Add Tag', 'description': 'Added tag \'' + tag_name + '\'', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(tci.id), ntype), 'reject_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(tci.id), ntype) } ChangeRequest(**change_request_params).save() response = { 'message': 'success', 'new_labels': new_labels, 'duplicate_labels': [ l.class_instance.name for l in duplicate_labels if l not in deleted_labels ], 'deleted_labels': [l.class_instance.name for l in deleted_labels], } # Check if any labels on this node violate cardinality restrictions on # its skeleton. if 'treenode' == ntype: limited_labels = { l: SKELETON_LABEL_CARDINALITY[l] for l in new_tags if l in SKELETON_LABEL_CARDINALITY } if limited_labels: ll_names, ll_maxes = zip(*limited_labels.items()) cursor = connection.cursor() cursor.execute( """ SELECT ll.name, COUNT(tci.treenode_id), ll.max FROM class_instance ci, treenode_class_instance tci, treenode tn, unnest(%s::text[], %s::integer[]) AS ll (name, max) WHERE ci.name = ll.name AND ci.project_id = %s AND ci.class_id = %s AND tci.class_instance_id = ci.id AND tci.relation_id = %s AND tn.id = tci.treenode_id AND tn.skeleton_id = %s GROUP BY ll.name, ll.max HAVING COUNT(tci.treenode_id) > ll.max """, (list(ll_names), list(ll_maxes), p.id, label_class.id, labeled_as_relation.id, node.skeleton_id)) if cursor.rowcount: response['warning'] = 'The skeleton has too many of the following tags: ' + \ ', '.join('{0} ({1}, max. {2})'.format(*row) for row in cursor.fetchall()) return JsonResponse(response)
def delete_treenode(request, project_id=None): """ Deletes a treenode. If the skeleton has a single node, deletes the skeleton and its neuron. Returns the parent_id, if any.""" treenode_id = int(request.POST.get('treenode_id', -1)) # Raise an exception if the user doesn't have permission to edit the # treenode. can_edit_or_fail(request.user, treenode_id, 'treenode') # Raise an Exception if the user doesn't have permission to edit the neuron # the skeleton of the treenode is modeling. can_edit_treenode_or_fail(request.user, project_id, treenode_id) treenode = Treenode.objects.get(pk=treenode_id) parent_id = treenode.parent_id response_on_error = '' deleted_neuron = False try: if not parent_id: # This treenode is root. response_on_error = 'Could not retrieve children for ' \ 'treenode #%s' % treenode_id n_children = Treenode.objects.filter(parent=treenode).count() response_on_error = "Could not delete root node" if n_children > 0: # TODO yes you can, the new root is the first of the children, # and other children become independent skeletons raise Exception("You can't delete the root node when it " "has children.") # Get the neuron before the skeleton is deleted. It can't be # accessed otherwise anymore. neuron = ClassInstance.objects.get( project_id=project_id, cici_via_b__relation__relation_name='model_of', cici_via_b__class_instance_a=treenode.skeleton) # Remove the original skeleton. It is OK to remove it if it only had # one node, even if the skeleton's user does not match or the user # is not superuser. Delete the skeleton, which triggers deleting # the ClassInstanceClassInstance relationship with neuron_id response_on_error = 'Could not delete skeleton.' # Extra check for errors, like having two root nodes count = Treenode.objects.filter(skeleton_id=treenode.skeleton_id) \ .count() if 1 == count: # deletes as well treenodes that refer to the skeleton ClassInstance.objects.filter(pk=treenode.skeleton_id) \ .delete() else: return HttpResponse(json.dumps({"error": "Can't delete " \ "isolated node: erroneously, its skeleton contains more " \ "than one treenode! Check for multiple root nodes."})) # If the neuron modeled by the skeleton of the treenode is empty, # delete it. response_on_error = 'Could not delete neuron #%s' % neuron.id deleted_neuron = _delete_if_empty(neuron.id) else: # Treenode is not root, it has a parent and perhaps children. # Reconnect all the children to the parent. response_on_error = 'Could not update parent id of children nodes' Treenode.objects.filter(parent=treenode) \ .update(parent=treenode.parent) # Remove treenode response_on_error = 'Could not delete treenode.' Treenode.objects.filter(pk=treenode_id).delete() return HttpResponse( json.dumps({ 'deleted_neuron': deleted_neuron, 'parent_id': parent_id, 'skeleton_id': treenode.skeleton_id, 'success': "Removed treenode successfully." })) except Exception as e: raise Exception(response_on_error + ': ' + str(e))
def label_update(request, project_id=None, location_id=None, ntype=None): """ location_id is the ID of a treenode or connector. ntype is either 'treenode' or 'connector'. """ labeled_as_relation = Relation.objects.get(project=project_id, relation_name='labeled_as') p = get_object_or_404(Project, pk=project_id) # TODO will FAIL when a tag contains a coma by itself new_tags = request.POST['tags'].split(',') delete_existing_labels = request.POST.get('delete_existing', 'true') == 'true' kwargs = {'relation': labeled_as_relation, 'class_instance__class_column__class_name': 'label'} table = get_link_model(ntype) if 'treenode' == ntype: kwargs['treenode__id'] = location_id node = Treenode.objects.get(id=location_id) elif 'connector' == ntype: kwargs['connector__id'] = location_id node = Connector.objects.get(id=location_id) if not table: raise Http404('Unknown node type: "%s"' % (ntype,)) # Get the existing list of tags for the tree node/connector and delete any # that are not in the new list. existing_labels = table.objects.filter(**kwargs).select_related('class_instance') existing_names = set(ele.class_instance.name for ele in existing_labels) duplicate_labels = table.objects.filter(**kwargs).exclude(class_instance__name__in=new_tags).select_related('class_instance') other_labels = [] deleted_labels = [] if delete_existing_labels: # Iterate over all labels that should get deleted to check permission # on each one. Remember each label that couldn't be deleted in the # other_labels array. for l in duplicate_labels: try: can_edit_or_fail(request.user, l.id, table._meta.db_table) if remove_label(l.id, ntype): deleted_labels.append(l) else: other_labels.append(l) except: other_labels.append(l) # Create change requests for labels associated to the treenode by other users for label in other_labels: change_request_params = { 'type': 'Remove Tag', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'description': "Remove tag '%s'" % label.class_instance.name, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(label.id), ntype), 'approve_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(label.id), ntype) } ChangeRequest(**change_request_params).save() # Add any new labels. label_class = Class.objects.get(project=project_id, class_name='label') kwargs = {'user': request.user, 'project': p, 'relation': labeled_as_relation, ntype: node} new_labels = [] for tag_name in new_tags: if len(tag_name) > 0 and tag_name not in existing_names: # Make sure the tag instance exists existing_tags = tuple(ClassInstance.objects.filter( project=p, name=tag_name, class_column=label_class)) if len(existing_tags) < 1: tag = ClassInstance( project=p, name=tag_name, user=request.user, class_column=label_class) tag.save() else: tag = existing_tags[0] # Associate the tag with the treenode/connector. kwargs['class_instance'] = tag tci = table(**kwargs) # creates new TreenodeClassInstance or ConnectorClassInstance tci.save() new_labels.append(tag_name) if node.user != request.user: # Inform the owner of the node that the tag was added and give them the option of removing it. change_request_params = { 'type': 'Add Tag', 'description': 'Added tag \'' + tag_name + '\'', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(tci.id), ntype), 'reject_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(tci.id), ntype) } ChangeRequest(**change_request_params).save() response = { 'message': 'success', 'new_labels': new_labels, 'duplicate_labels': [l.class_instance.name for l in duplicate_labels if l not in deleted_labels], 'deleted_labels': [l.class_instance.name for l in deleted_labels], } # Check if any labels on this node violate cardinality restrictions on # its skeleton. if 'treenode' == ntype: limited_labels = {l: SKELETON_LABEL_CARDINALITY[l] for l in new_tags if l in SKELETON_LABEL_CARDINALITY} if limited_labels: ll_names, ll_maxes = zip(*limited_labels.items()) cursor = connection.cursor() cursor.execute(""" SELECT ll.name, COUNT(tci.treenode_id), ll.max FROM class_instance ci, treenode_class_instance tci, treenode tn, unnest(%s::text[], %s::integer[]) AS ll (name, max) WHERE ci.name = ll.name AND ci.project_id = %s AND ci.class_id = %s AND tci.class_instance_id = ci.id AND tci.relation_id = %s AND tn.id = tci.treenode_id AND tn.skeleton_id = %s GROUP BY ll.name, ll.max HAVING COUNT(tci.treenode_id) > ll.max """, ( list(ll_names), list(ll_maxes), p.id, label_class.id, labeled_as_relation.id, node.skeleton_id)) if cursor.rowcount: response['warning'] = 'The skeleton has too many of the following tags: ' + \ ', '.join('{0} ({1}, max. {2})'.format(*row) for row in cursor.fetchall()) return JsonResponse(response)
def delete_sampler(request, project_id, sampler_id): """Delete a sampler if permissions allow it. If the sampler was created with allowing the creation of new boundary nodes, these nodes are removed by default if they have not been modified since their insertion. This can optionally be disabled using the <delete_created_nodes> parameter. --- parameters: - name: delete_created_nodes description: | Optional flag to disable automatic removal of untouched nodes created for this sampler's intervals. type: boolean default: true paramType: form required: false """ can_edit_or_fail(request.user, sampler_id, "catmaid_sampler") sampler = Sampler.objects.get(id=sampler_id) n_deleted_nodes = 0 delete_created_nodes = get_request_bool(request.POST, 'delete_created_nodes', True) if delete_created_nodes and sampler.create_interval_boundaries: labeled_as_relation = Relation.objects.get(project=project_id, relation_name='labeled_as') label_class = Class.objects.get(project=project_id, class_name='label') label_class_instance = ClassInstance.objects.get(project=project_id, class_column=label_class, name=SAMPLER_CREATED_CLASS) # If the sampler was parameterized to created interval boundary nodes, # these nodes can now be removed if they are still collinear with their # child and parent node and have not been touched. These nodes are all # nodes that are referenced by intervals of this sampler that have the # SAMPLER_CREATED_CLASS tag with their creation time being the same as the # edition time. Such nodes can only be sampler interval start/end nodes. params = { 'project_id': project_id, 'sampler_id': sampler_id, 'labeled_as_rel': labeled_as_relation.id, 'label_class': label_class.id, 'label_class_instance': label_class_instance.id } cursor = connection.cursor() # Get all created sampler interval boundary treenodes that have been # created during sampler creation. The result will also contain parent # and child locations. We need to set extra_float_digits to get enough # precision for the location data to do a collinearity test. cursor.execute(""" SET extra_float_digits = 3; WITH sampler_treenode AS ( -- Get all treenodes linked to intervals of this sampler. Only -- select those nodes that are referenced by no other sampler -- (using an anti join). SELECT DISTINCT all_added_nodes.id FROM ( SELECT DISTINCT UNNEST(ARRAY[i.start_node_id, i.end_node_id]) AS id FROM catmaid_samplerinterval i JOIN catmaid_samplerdomain d ON i.domain_id = d.id WHERE d.sampler_id = %(sampler_id)s ) all_added_nodes JOIN catmaid_samplerinterval csi ON csi.start_node_id = all_added_nodes.id OR csi.end_node_id = all_added_nodes.id JOIN catmaid_samplerdomain csd ON csd.id = csi.domain_id GROUP BY all_added_nodes.id HAVING COUNT(DISTINCT csd.sampler_id) = 1 ), sampler_created_treenode AS ( -- Find all treenodes that were created by the sampler and are -- undmodified. SELECT st.id FROM sampler_treenode st JOIN treenode_class_instance tci ON st.id = tci.treenode_id WHERE tci.relation_id = %(labeled_as_rel)s AND tci.class_instance_id = %(label_class_instance)s ) SELECT t.id, t.location_x, t.location_y, t.location_z, c.id, c.location_x, c.location_y, c.location_z, p.id, p.location_x, p.location_y, p.location_z FROM ( -- Make sure we look only at nodes that don't have multiple nodes. SELECT st.id FROM treenode tt JOIN sampler_created_treenode st ON tt.parent_id = st.id GROUP BY st.id HAVING count(*) = 1 ) non_branch_treenodes(id) JOIN treenode t ON t.id = non_branch_treenodes.id JOIN treenode p ON p.id = t.parent_id JOIN treenode c ON c.parent_id = t.id WHERE t.project_id = %(project_id)s; """, params) created_treenodes = [r for r in cursor.fetchall()] if created_treenodes: added_node_index = dict((n[0], n) for n in created_treenodes) # Find those created treenodes that are collinear with their parent and # child node. If they are, remove those nodes. Ideally, we would move # the collinearity test into SQL as well. nodes_to_remove = [] parents_to_fix = [] child, node, parent = Point3D(0, 0, 0), Point3D(0, 0, 0), Point3D(0, 0, 0) for n in created_treenodes: n_id, node.x, node.y, node.z = n[0], n[1], n[2], n[3] c_id, child.x, child.y, child.z = n[4], n[5], n[6], n[7] p_id, parent.x, parent.y, parent.z = n[8], n[9], n[10], n[11] child_is_original_node = c_id not in added_node_index if is_collinear(child, parent, node, True, 1.0): nodes_to_remove.append(n_id) # Only update nodes that don't get deleted anyway if child_is_original_node: parents_to_fix.append((c_id, p_id)) else: parents_to_fix.append((n_id, p_id)) # Update parent in formation in parent relation updates. If present # parent IDs point to a removed node, the next real parent will be # used instead. parent_update = [] for n, (c_id, p_id) in enumerate(parents_to_fix): parent_is_persistent = p_id not in added_node_index if parent_is_persistent: parent_update.append((c_id, p_id)) else: # Find next existing node upstream new_parent_id = p_id while not parent_is_persistent: parent_is_persistent = new_parent_id not in nodes_to_remove node_data = added_node_index.get(new_parent_id) # An added node would be used if it is not removed, e.g. # du to not being collinear anymore. if node_data and not parent_is_persistent: new_parent_id = node_data[8] else: parent_update.append((c_id, new_parent_id)) if nodes_to_remove: query_parts = [] params = [] if parent_update: update_nodes_template = ",".join("(%s, %s)" for _ in parent_update) update_nodes_flattened = list(chain.from_iterable(parent_update)) query_parts.append(""" UPDATE treenode SET parent_id = nodes_to_update.parent_id FROM (VALUES {}) nodes_to_update(child_id, parent_id) WHERE treenode.id = nodes_to_update.child_id; """.format(update_nodes_template)) params = update_nodes_flattened delete_nodes_template = ",".join("(%s)" for _ in nodes_to_remove) query_parts.append(""" DELETE FROM treenode WHERE id IN ( SELECT t.id FROM treenode t JOIN (VALUES {}) to_delete(id) ON t.id = to_delete.id ) RETURNING id; """.format(delete_nodes_template)) params = params + nodes_to_remove cursor.execute("\n".join(query_parts), params) deleted_node_ids = [r[0] for r in cursor.fetchall()] n_deleted_nodes = len(deleted_node_ids) sampler.delete() return JsonResponse({ 'deleted_sampler_id': sampler_id, 'deleted_interval_nodes': n_deleted_nodes })
def label_update(request, project_id=None, location_id=None, ntype=None): """ location_id is the ID of a treenode or connector. ntype is either 'treenode' or 'connector'. """ labeled_as_relation = Relation.objects.get(project=project_id, relation_name='labeled_as') p = get_object_or_404(Project, pk=project_id) # TODO will FAIL when a tag contains a coma by itself new_tags = request.POST['tags'].split(',') delete_existing_labels = request.POST.get('delete_existing', 'true') == 'true' kwargs = {'relation': labeled_as_relation, 'class_instance__class_column__class_name': 'label'} table = get_link_model(ntype) if 'treenode' == ntype: kwargs['treenode__id'] = location_id node = Treenode.objects.get(id=location_id) elif 'connector' == ntype: kwargs['connector__id'] = location_id node = Connector.objects.get(id=location_id) if not table: raise Http404('Unknown node type: "%s"' % (ntype,)) # Get the existing list of tags for the tree node/connector and delete any # that are not in the new list. existingLabels = table.objects.filter(**kwargs).select_related('class_instance__name') existing_names = set(ele.class_instance.name for ele in existingLabels) labels_to_delete = table.objects.filter(**kwargs).exclude(class_instance__name__in=new_tags) if delete_existing_labels: # Iterate over all labels that should get deleted to check permission # on each one. Remember each label that couldn't be deleted in the # other_labels array. other_labels = [] deleted_labels = [] for l in labels_to_delete: try: can_edit_or_fail(request.user, l.id, table._meta.db_table) if remove_label(l.id, ntype): deleted_labels.append(l) else: other_labels.append(l) except: other_labels.append(l) # Create change requests for labels associated to the treenode by other users for label in other_labels: change_request_params = { 'type': 'Remove Tag', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'description': "Remove tag '%s'" % label.class_instance.name, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(label.id), ntype), 'approve_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(label.id), ntype) } ChangeRequest(**change_request_params).save() # Add any new labels. label_class = Class.objects.get(project=project_id, class_name='label') kwargs = {'user': request.user, 'project': p, 'relation': labeled_as_relation, ntype: node} for tag_name in new_tags: if len(tag_name) > 0 and tag_name not in existing_names: # Make sure the tag instance exists existing_tags = tuple(ClassInstance.objects.filter( project=p, name=tag_name, class_column=label_class)) if len(existing_tags) < 1: tag = ClassInstance( project=p, name=tag_name, user=request.user, class_column=label_class) tag.save() else: tag = existing_tags[0] # Associate the tag with the treenode/connector. kwargs['class_instance'] = tag tci = table(**kwargs) # creates new TreenodeClassInstance or ConnectorClassInstance tci.save() if node.user != request.user: # Inform the owner of the node that the tag was added and give them the option of removing it. change_request_params = { 'type': 'Add Tag', 'description': 'Added tag \'' + tag_name + '\'', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(tci.id), ntype), 'reject_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(tci.id), ntype) } ChangeRequest(**change_request_params).save() return HttpResponse(json.dumps({'message': 'success'}), content_type='text/json')
def label_update(request, project_id=None, location_id=None, ntype=None): """ location_id is the ID of a treenode or connector. ntype is either 'treenode' or 'connector'. """ labeled_as_relation = Relation.objects.get(project=project_id, relation_name='labeled_as') p = get_object_or_404(Project, pk=project_id) # TODO will FAIL when a tag contains a coma by itself new_tags = request.POST['tags'].split(',') delete_existing_labels = request.POST.get('delete_existing', 'true') == 'true' kwargs = { 'relation': labeled_as_relation, 'class_instance__class_column__class_name': 'label' } table = get_link_model(ntype) if 'treenode' == ntype: kwargs['treenode__id'] = location_id node = Treenode.objects.get(id=location_id) elif 'connector' == ntype: kwargs['connector__id'] = location_id node = Connector.objects.get(id=location_id) if not table: raise Http404('Unknown node type: "%s"' % (ntype, )) # Get the existing list of tags for the tree node/connector and delete any # that are not in the new list. existingLabels = table.objects.filter( **kwargs).select_related('class_instance__name') existing_names = set(ele.class_instance.name for ele in existingLabels) labels_to_delete = table.objects.filter(**kwargs).exclude( class_instance__name__in=new_tags) if delete_existing_labels: # Iterate over all labels that should get deleted to check permission # on each one. Remember each label that couldn't be deleted in the # other_labels array. other_labels = [] deleted_labels = [] for l in labels_to_delete: try: can_edit_or_fail(request.user, l.id, table._meta.db_table) if remove_label(l.id, ntype): deleted_labels.append(l) else: other_labels.append(l) except: other_labels.append(l) # Create change requests for labels associated to the treenode by other users for label in other_labels: change_request_params = { 'type': 'Remove Tag', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'description': "Remove tag '%s'" % label.class_instance.name, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(label.id), ntype), 'approve_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(label.id), ntype) } ChangeRequest(**change_request_params).save() # Add any new labels. label_class = Class.objects.get(project=project_id, class_name='label') kwargs = { 'user': request.user, 'project': p, 'relation': labeled_as_relation, ntype: node } for tag_name in new_tags: if len(tag_name) > 0 and tag_name not in existing_names: # Make sure the tag instance exists existing_tags = tuple( ClassInstance.objects.filter(project=p, name=tag_name, class_column=label_class)) if len(existing_tags) < 1: tag = ClassInstance(project=p, name=tag_name, user=request.user, class_column=label_class) tag.save() else: tag = existing_tags[0] # Associate the tag with the treenode/connector. kwargs['class_instance'] = tag tci = table( **kwargs ) # creates new TreenodeClassInstance or ConnectorClassInstance tci.save() if node.user != request.user: # Inform the owner of the node that the tag was added and give them the option of removing it. change_request_params = { 'type': 'Add Tag', 'description': 'Added tag \'' + tag_name + '\'', 'project': p, 'user': request.user, 'recipient': node.user, 'location': Double3D(node.location_x, node.location_y, node.location_z), ntype: node, 'validate_action': 'from catmaid.control.label import label_exists\n' + 'is_valid = label_exists(%s, "%s")' % (str(tci.id), ntype), 'reject_action': 'from catmaid.control.label import remove_label\n' + 'remove_label(%s, "%s")' % (str(tci.id), ntype) } ChangeRequest(**change_request_params).save() return HttpResponse(json.dumps({'message': 'success'}), content_type='application/json')
def delete(self, request, project_id): """Delete a list of landmarks including the linked locations, if they are not used by other landmarks. --- parameters: - name: project_id description: The project the landmark is part of. type: integer paramType: path required: true - name: landmark_ids description: The landmarks to remove. required: true type: integer paramType: form - name: keep_points description: Don't delete points. required: false type: boolean defaultValue: false paramType: form """ keep_points = request.query_params.get('keep_points', 'false') == 'true' landmark_ids = get_request_list(request.query_params, 'landmark_ids', map_fn=int) for l in landmark_ids: can_edit_or_fail(request.user, l, 'class_instance') annotated_with_relation = Relation.objects.get( project_id=project_id, relation_name='annotated_with') point_ids = set() if not keep_points: point_landmark_links = PointClassInstance.objects.filter( project_id=project_id, class_instance_id__in=landmark_ids, relation=annotated_with_relation) # These are the landmark's lined points point_ids = set(pll.point_id for pll in point_landmark_links) landmark_class = Class.objects.get(project_id=project_id, class_name="landmark") landmarks = ClassInstance.objects.filter(pk__in=landmark_ids, project_id=project_id, class_column=landmark_class) if len(landmark_ids) != len(landmarks): raise ValueError("Could not find all landmark IDs") landmarks.delete() if not keep_points: remaining_pll = set( PointClassInstance.objects.filter( project_id=project_id, point_id__in=point_ids, relation=annotated_with_relation).values_list('point_id', flat=True)) points_to_delete = point_ids - remaining_pll Point.objects.filter(project_id=project_id, pk__in=points_to_delete).delete() serializer = BasicClassInstanceSerializer(landmarks, many=True) return Response(serializer.data)