def get_plant_properties(plant_id: int,
                         taxon_id: int = None,
                         db: Session = Depends(get_db)):
    """reads a plant's property values from db; plus it's taxon's property values"""

    load_properties = LoadProperties()
    categories = load_properties.get_properties_for_plant(plant_id, db)

    categories_taxon = load_properties.get_properties_for_taxon(
        taxon_id, db) if taxon_id else []

    make_list_items_json_serializable(categories)
    make_list_items_json_serializable(categories_taxon)

    results = {
        'propertyCollections': {
            "categories": categories
        },
        'plant_id':
        plant_id,
        'propertyCollectionsTaxon': {
            "categories": categories_taxon
        },
        'taxon_id':
        taxon_id,
        'action':
        'Get',
        'resource':
        'PropertyTaxaResource',
        'message':
        get_message(
            f"Receiving properties for plant {plant_id} from database.")
    }
    return results
Beispiel #2
0
async def get_plants(db: Session = Depends(get_db)):
    """read (almost unfiltered) plants information from db"""
    # select plants from database
    # filter out hidden ("deleted" in frontend but actually only flagged hidden) plants
    query = db.query(Plant)
    if config.filter_hidden:
        # sqlite does not like "is None" and pylint doesn't like "== None"
        query = query.filter((Plant.hide.is_(False)) | (Plant.hide.is_(None)))

    if DEMO_MODE_RESTRICT_TO_N_PLANTS:
        query = query.limit(DEMO_MODE_RESTRICT_TO_N_PLANTS)

    plants_obj = query.all()
    plants_list = [p.as_dict() for p in plants_obj]

    make_list_items_json_serializable(plants_list)
    results = {
        'action': 'Get plants',
        'resource': 'PlantResource',
        'message':
        get_message(f"Loaded {len(plants_list)} plants from database."),
        'PlantsCollection': plants_list
    }

    return results
Beispiel #3
0
def delete_plant(request: Request,
                 data: PPlantsDeleteRequest,
                 db: Session = Depends(get_db)):
    """tag deleted plant as 'hide' in database"""

    args = data

    record_update: Plant = db.query(Plant).filter_by(
        plant_name=args.plant).first()
    if not record_update:
        logger.error(
            f'Plant to be deleted not found in database: {args.plant}.')
        throw_exception(
            f'Plant to be deleted not found in database: {args.plant}.',
            request=request)
    record_update.hide = True
    db.commit()

    logger.info(message := f'Deleted plant {args.plant}')
    results = {
        'action':
        'Deleted plant',
        'resource':
        'PlantResource',
        'message':
        get_message(message,
                    description=f'Plant name: {args.plant}\nHide: True')
    }

    return results
Beispiel #4
0
async def upload_images(request: Request, db: Session = Depends(get_db)):
    """upload new image(s)
    todo: switch key in supplied plants list to id"""
    # the ui5 uploader control does somehow not work with the expected form/multipart format expected
    # via fastapi argument files = List[UploadFile] = File(...)
    # therefore, we directly go on the starlette request object
    form = await request.form()
    additional_data = json.loads(form.get('files-data'))
    # noinspection PyTypeChecker
    files: List[UploadFile] = form.getlist('files[]')

    # validate arguments manually as pydantic doesn't trigger here
    try:
        PImageUploadedMetadata(**additional_data)
    except ValidationError as err:
        throw_exception(str(err), request=request)

    # todo: get rid of that key/text/keyword dict
    plants = tuple(
        {
            'key': p,
            'text': p
        } for p in
        additional_data['plants'])  # if 'plants' in additional_data else []
    keywords = tuple({
        'keyword': k,
        'text': k
    } for k in additional_data['keywords'])
    # if 'keywords' in additional_data else []

    # remove duplicates (filename already exists in file system)
    duplicate_filenames = remove_files_already_existing(files, RESIZE_SUFFIX)

    photos: List[Photo] = await _save_image_files(files=files,
                                                  request=request,
                                                  plants=plants,
                                                  keywords=keywords)
    photo_files_ext = _get_pimages_from_photos(photos, db=db)

    msg = get_message(f'Saved {len(files)} images.' +
                      (' Duplicates found.' if duplicate_filenames else ''),
                      message_type=MessageType.WARNING
                      if duplicate_filenames else MessageType.INFORMATION,
                      description=f'Saved: {[p.filename for p in files]}.'
                      f'\nSkipped Duplicates: {duplicate_filenames}.')
    logger.info(msg['message'])
    results = {
        'action': 'Uploaded',
        'resource': 'ImageResource',
        'message': msg,
        'images': photo_files_ext
    }

    return results
async def modify_plant_properties(data: PPropertiesModifiedPlant,
                                  db: Session = Depends(get_db)):
    """save plant properties"""

    SaveProperties().save_properties(data.modifiedPropertiesPlants, db=db)
    results = {
        'action': 'Update',
        'resource': 'PropertyResource',
        'message': get_message(f'Updated properties in database.')
    }

    return results
async def refresh_photo_directory():
    """recreates the photo directory, i.e. re-reads directory, creates missing thumbnails etc."""
    with lock_photo_directory:
        get_photo_directory().refresh_directory()

    logger.info(message := f'Refreshed photo directory')
    results = {
        'action': 'Function refresh Photo Directory',
        'resource': 'RefreshPhotoDirectoryResource',
        'message': get_message(message)
    }

    return results
Beispiel #7
0
async def get_selection_data(db: Session = Depends(get_db)):
    """build & return taxon tree for advanced filtering"""
    taxon_tree = build_taxon_tree(db)
    make_list_items_json_serializable(taxon_tree)

    results = {
        'action': 'Get taxon tree',
        'resource': 'SelectionResource',
        'message': get_message(f"Loaded selection data."),
        'Selection': {
            'TaxonTree': taxon_tree
        }
    }

    return results
async def modify_taxon_properties(data: PPropertiesModifiedTaxon,
                                  db: Session = Depends(get_db)):
    """taxon properties; note: there's no get method for taxon properties; they are read with the plant's
        properties
        save taxon properties"""

    SavePropertiesTaxa().save_properties(
        properties_modified=data.modifiedPropertiesTaxa, db=db)
    results = {
        'action': 'Update',
        'resource': 'PropertyTaxaResource',
        'message': get_message(f'Updated properties for taxa in database.')
    }

    return results
Beispiel #9
0
async def delete_image(request: Request, photo: PImageDelete):
    """move the file that should be deleted to another folder (not actually deleted, currently)"""

    old_path = photo.path_full_local
    if not os.path.isfile(old_path):
        logger.error(
            err_msg := f"File selected to be deleted not found: {old_path}")
        throw_exception(err_msg, request=request)

    filename = os.path.basename(old_path)
    new_path = os.path.join(PATH_DELETED_PHOTOS, filename)

    try:
        os.replace(
            src=old_path,
            dst=new_path)  # silently overwrites if privileges are sufficient
    except OSError as e:
        logger.error(err_msg :=
                     f'OSError when moving file {old_path} to {new_path}',
                     exc_info=e)
        throw_exception(err_msg,
                        description=f'Filename: {os.path.basename(old_path)}',
                        request=request)
    logger.info(f'Moved file {old_path} to {new_path}')

    # remove from PhotoDirectory cache
    with lock_photo_directory:
        photo_directory = get_photo_directory(instantiate=False)
        if photo_directory:
            photo_obj = photo_directory.get_photo(photo.path_full_local)
            photo_directory.remove_image_from_directory(photo_obj)

    results = {
        'action':
        'Deleted',
        'resource':
        'ImageResource',
        'message':
        get_message(f'Successfully deleted image',
                    description=f'Filename: {os.path.basename(old_path)}'),
        'photo':
        photo
    }

    # send the photo back to frontend; it will be removed from json model there
    return results
Beispiel #10
0
def rename_plant(request: Request,
                 data: PPlantsRenameRequest,
                 db: Session = Depends(get_db)):
    """we use the put method to rename a plant"""
    args = data

    plant_obj = db.query(Plant).filter(
        Plant.plant_name == args.OldPlantName).first()
    if not plant_obj:
        throw_exception(f"Can't find plant {args.OldPlantName}",
                        request=request)

    if db.query(Plant).filter(Plant.plant_name == args.NewPlantName).first():
        throw_exception(f"Plant already exists: {args.NewPlantName}",
                        request=request)

    # rename plant name
    plant_obj.plant_name = args.NewPlantName
    plant_obj.last_update = datetime.datetime.now()

    # most difficult task: exif tags use plant name not id; we need to change each plant name occurence
    # in images' exif tags
    count_modified_images = rename_plant_in_image_files(
        args.OldPlantName, args.NewPlantName)

    # only after image modifications have gone well, we can commit changes to database
    db.commit()

    create_history_entry(description=f"Renamed to {args.NewPlantName}",
                         db=db,
                         plant_id=plant_obj.id,
                         plant_name=args.OldPlantName,
                         commit=False)

    logger.info(f'Modified {count_modified_images} images.')
    results = {
        'action':
        'Renamed plant',
        'resource':
        'PlantResource',
        'message':
        get_message(f'Renamed {args.OldPlantName} to {args.NewPlantName}',
                    description=f'Modified {count_modified_images} images.')
    }

    return results
Beispiel #11
0
async def get_property_names(db: Session = Depends(get_db)):
    category_obj = db.query(PropertyCategory).all()
    categories = {}
    for cat in category_obj:
        categories[cat.category_name] = [{
            'property_name': p.property_name,
            'property_name_id': p.id,
            'countPlants': len(p.property_values)
        } for p in cat.property_names]

    results = {
        'action': 'Get',
        'resource': 'PropertyNameResource',
        'propertiesAvailablePerCategory': categories,
        'message': get_message(f"Receiving Property Names from database.")
    }

    return results
Beispiel #12
0
async def upload_images_plant(plant_id: int,
                              request: Request,
                              db: Session = Depends(get_db)):
    """
    upload images and directly assign them to supplied plant; no keywords included
    # the ui5 uploader control does somehow not work with the expected form/multipart format expected
    # via fastapi argument files = List[UploadFile] = File(...)
    # therefore, we directly go on the starlette request object
    """
    form = await request.form()
    # noinspection PyTypeChecker
    files: List[UploadFile] = form.getlist('files[]')

    # remove duplicates (filename already exists in file system)
    duplicate_filenames = remove_files_already_existing(files, RESIZE_SUFFIX)

    plant_name = Plant.get_plant_name_by_plant_id(plant_id,
                                                  db,
                                                  raise_exception=True)

    photos: List[Photo] = await _save_image_files(files=files,
                                                  request=request,
                                                  plants=({
                                                      'key': plant_name,
                                                      'text': plant_name
                                                  }, ))
    photo_files_ext = _get_pimages_from_photos(photos, db=db)

    msg = get_message(f'Saved {len(files)} images.' +
                      (' Duplicates found.' if duplicate_filenames else ''),
                      message_type=MessageType.WARNING
                      if duplicate_filenames else MessageType.INFORMATION,
                      description=f'Saved: {[p.filename for p in files]}.'
                      f'\nSkipped Duplicates: {duplicate_filenames}.')
    logger.info(msg['message'])
    results = {
        'action': 'Uploaded',
        'resource': 'ImageResource',
        'message': msg,
        'images': photo_files_ext
    }

    return results
Beispiel #13
0
def modify_plants(data: PPlantsUpdateRequest, db: Session = Depends(get_db)):
    """update existing or create new plants"""
    plants_modified = data.PlantsCollection

    # update plants
    plants_saved = update_plants_from_list_of_dicts(plants_modified, db)

    # serialize updated/created plants to refresh data in frontend
    plants_list = [p.as_dict() for p in plants_saved]
    make_list_items_json_serializable(plants_list)

    logger.info(message := f"Saved updates for {len(plants_modified)} plants.")
    results = {
        'action': 'Saved Plants',
        'resource': 'PlantResource',
        'message': get_message(message),
        'plants': plants_list
    }  # return the updated/created plants

    return results
Beispiel #14
0
async def get_events(plant_id: int, db: Session = Depends(get_db)):
    """returns events from event database table
    imports: plant_id
    exports: see PResultsEventResource
    """
    results = []
    # might be a newly created plant with no existing events, yet
    event_objs = Event.get_events_by_plant_id(plant_id, db)
    for event_obj in event_objs:
        results.append(event_obj.as_dict())

    logger.info(
        m :=
        f'Receiving {len(results)} events for {Plant.get_plant_name_by_plant_id(plant_id, db)}.'
    )
    results = {
        'events': results,
        'message': get_message(m, message_type=MessageType.DEBUG)
    }

    return results
Beispiel #15
0
async def get_images(untagged: bool = False, db: Session = Depends(get_db)):
    """
    get image information for all plants from images and their exif tags including plants and keywords
    optionally, request only images that have no plants tagged, yet
    """
    # instantiate photo directory if required, get photos in external format from files exif data
    # with lock_photo_directory:
    #     photo_files_all = get_photo_directory().get_photo_files_ext()
    with lock_photo_directory:
        if untagged:
            photo_files_all = get_photo_directory().get_photo_files_untagged()
        else:
            photo_files_all = get_photo_directory().get_photo_files()

    photo_files_ext = _get_pimages_from_photos(photo_files_all, db=db)

    # filter out images whose only plants are configured to be inactive
    inactive_plants = set(
        p.plant_name for p in db.query(Plant.plant_name).filter_by(hide=True))
    photo_files_ext = [
        f for f in photo_files_ext
        if len(f.plants) != 1 or f.plants[0].key not in inactive_plants
    ]
    logger.debug(
        f'Filter out {len(photo_files_all) - len(photo_files_ext)} images due to Hide flag of the only tagged '
        f'plant.')

    # make serializable
    # make_list_items_json_serializable(photo_files_ext)
    logger.info(f'Returned {len(photo_files_ext)} images.')
    results = {
        'ImagesCollection':
        photo_files_ext,
        'message':
        get_message('Loaded images from backend.',
                    description=f'Count: {len(photo_files_ext)}')
    }

    return results
def get_proposals(request: Request,
                  entity_id: ProposalEntity,
                  db: Session = Depends(get_db)):
    """returns proposals for selection tables"""

    results = {}
    if entity_id == ProposalEntity.SOIL:
        results = {'SoilsCollection': [], 'ComponentsCollection': []}
        # soil mixes
        soils = db.query(Soil).all()
        for soil in soils:
            soil_dict = soil.as_dict()
            soil_dict['components'] = [{
                'component_name': c.soil_component.component_name,
                'portion': c.portion
            } for c in soil.soil_to_component_associations]

            results['SoilsCollection'].append(soil_dict)

        # soil components for new mixes
        components = db.query(SoilComponent).all()
        results['ComponentsCollection'] = [{
            'component_name': c.component_name
        } for c in components]

    elif entity_id == ProposalEntity.NURSERY:
        # get distinct nurseries/sources, sorted by last update
        nurseries_tuples = db.query(Plant.nursery_source) \
            .order_by(Plant.last_update.desc()) \
            .distinct(Plant.nursery_source)\
            .filter(Plant.nursery_source.isnot(None)).all()
        if not nurseries_tuples:
            results = {'NurseriesSourcesCollection': []}
        else:
            results = {
                'NurseriesSourcesCollection': [{
                    'name': n[0]
                } for n in nurseries_tuples]
            }

    elif entity_id == ProposalEntity.KEYWORD:
        # return collection of all distinct keywords used in images
        keywords_set = get_distinct_keywords_from_image_files()
        keywords_collection = [{
            'keyword': keyword
        } for keyword in keywords_set]
        results = {'KeywordsCollection': keywords_collection}

    elif entity_id == ProposalEntity.TRAIT_CATEGORY:
        # trait categories
        trait_categories = []
        t: Trait
        for t in TRAIT_CATEGORIES:
            # note: trait categories from config file are created in orm_tables.py if not existing upon start
            trait_category_obj = TraitCategory.get_cat_by_name(
                t, db, raise_exception=True)
            trait_categories.append(trait_category_obj.as_dict())
        results = {'TraitCategoriesCollection': trait_categories}

        # traits
        traits_query: List[Trait] = db.query(Trait).filter(
            Trait.trait_category.has(
                TraitCategory.category_name.in_(TRAIT_CATEGORIES)))
        traits = []
        for t in traits_query:
            t_dict = t.as_dict()
            t_dict['trait_category_id'] = t.trait_category_id
            t_dict['trait_category'] = t.trait_category.category_name
            traits.append(t_dict)
        results['TraitsCollection'] = traits

    else:
        throw_exception(f'Proposal entity {entity_id} not expected.',
                        request=request)

    results.update({
        'action':
        'Get',
        'resource':
        'ProposalResource',
        'message':
        get_message(
            f'Receiving proposal values for entity {entity_id} from backend.')
    })

    return results
Beispiel #17
0
async def get_taxa(db: Session = Depends(get_db)):
    """returns taxa from taxon database table"""
    taxa: List[Taxon] = db.query(Taxon).all()
    taxon_dict = {}
    for taxon in taxa:
        taxon_dict[taxon.id] = taxon.as_dict()
        if taxon.taxon_to_trait_associations:

            # build a dict of trait categories
            categories = {}
            for link in taxon.taxon_to_trait_associations:
                if link.trait.trait_category.id not in categories:
                    categories[link.trait.trait_category.id] = {
                        'id': link.trait.trait_category.id,
                        'category_name':
                        link.trait.trait_category.category_name,
                        'sort_flag': link.trait.trait_category.sort_flag,
                        'traits': []
                    }
                categories[link.trait.trait_category.id]['traits'].append({
                    'id':
                    link.trait.id,
                    'trait':
                    link.trait.trait,
                    # 'observed': link.observed,
                    'status':
                    link.status
                })

            # ui5 frontend requires a list for the json model
            taxon_dict[taxon.id]['trait_categories'] = list(
                categories.values())

        # images
        taxon_dict[taxon.id]['images'] = []
        if taxon.images:
            for link_obj in taxon.image_to_taxon_associations:
                image_obj = link_obj.image
                path_small = get_thumbnail_relative_path_for_relative_path(
                    image_obj.relative_path, size=config.size_thumbnail_image)
                taxon_dict[taxon.id]['images'].append({
                    'id':
                    image_obj.id,
                    'path_thumb':
                    path_small,
                    'path_original':
                    image_obj.relative_path,
                    'description':
                    link_obj.description
                })

        # distribution codes according to WGSRPD (level 3)
        taxon_dict[taxon.id]['distribution'] = {'native': [], 'introduced': []}
        for distribution_obj in taxon.distribution:
            if distribution_obj.establishment == 'Native':
                taxon_dict[taxon.id]['distribution']['native'].append(
                    distribution_obj.tdwg_code)
            elif distribution_obj.establishment == 'Introduced':
                taxon_dict[taxon.id]['distribution']['introduced'].append(
                    distribution_obj.tdwg_code)

        # occurence images
        taxon_dict[taxon.id]['occurrenceImages'] = [
            o.as_dict() for o in taxon.occurence_images
        ]

    logger.info(message := f'Received {len(taxon_dict)} taxa from database.')
    results = {
        'action': 'Get taxa',
        'resource': 'TaxonResource',
        'message': get_message(message),
        'TaxaDict': taxon_dict
    }

    # snake_case is converted to camelCase and date is converted to isoformat
    # results = PResultsGetTaxa(**results).dict(by_alias=True)
    return results
Beispiel #18
0
async def update_taxa(request: Request,
                      modified_taxa: PModifiedTaxa,
                      db: Session = Depends(get_db)):
    """two things can be changed in the taxon model, and these are modified in extensions here:
        - modified custom fields
        - traits"""
    modified_taxa = modified_taxa.ModifiedTaxaCollection

    for taxon_modified in modified_taxa:
        taxon: Taxon = db.query(Taxon).filter(
            Taxon.id == taxon_modified.id).first()
        if not taxon:
            logger.error(f'Taxon not found: {taxon.name}. Saving canceled.')
            throw_exception(f'Taxon not found: {taxon.name}. Saving canceled.',
                            request=request)

        taxon.custom_notes = taxon_modified.custom_notes
        update_traits(taxon, taxon_modified.trait_categories, db)

        # changes to images attached to the taxon
        # deleted images
        path_originals_saved = [image.path_original for image in taxon_modified.images] if \
            taxon_modified.images else []
        for image_obj in taxon.images:
            if image_obj.relative_path not in path_originals_saved:
                # don't delete image object, but only the association (image might be assigned to other events)
                db.delete([
                    link for link in taxon.image_to_taxon_associations
                    if link.image.relative_path == image_obj.relative_path
                ][0])

        # newly assigned images
        if taxon_modified.images:
            for image in taxon_modified.images:
                image_obj = db.query(Image).filter(
                    Image.relative_path == image.path_original).first()

                # not assigned to any event, yet
                if not image_obj:
                    image_obj = Image(relative_path=image.path_original)
                    db.add(image_obj)
                    db.flush()  # required to obtain id

                # update link table including the image description
                current_taxon_to_image_link = [
                    t for t in taxon.image_to_taxon_associations
                    if t.image == image_obj
                ]

                # insert link
                if not current_taxon_to_image_link:
                    link = ImageToTaxonAssociation(
                        image_id=image_obj.id,
                        taxon_id=taxon.id,
                        description=image.description)
                    db.add(link)
                    logger.info(
                        f'Image {image_obj.relative_path} assigned to taxon {taxon.name}'
                    )

                # update description
                elif current_taxon_to_image_link[
                        0].description != image.description:
                    current_taxon_to_image_link[
                        0].description = image.description
                    logger.info(
                        f'Update description of link between image {image_obj.relative_path} and taxon'
                        f' {taxon.name}')

    db.commit()

    results = {
        'action': 'Save taxa',
        'resource': 'TaxonResource',
        'message':
        get_message(f'Updated {len(modified_taxa)} taxa in database.')
    }

    logger.info(f'Updated {len(modified_taxa)} taxa in database.')
    return results
Beispiel #19
0
            logger.info(
                f'Updating changed image in PhotoDirectory Cache: {photo.path_full_local}'
            )
            photo.tag_keywords = [k.keyword for k in image_ext.keywords]
            photo.tag_authors_plants = [p.key for p in image_ext.plants]
            photo.tag_description = image_ext.description
            photo.write_exif_tags()

    results = {
        'action':
        'Saved',
        'resource':
        'ImageResource',
        'message':
        get_message(
            f"Saved updates for {len(modified_ext.ImagesCollection)} images.")
    }

    return results


@router.post("/images/", response_model=PResultsImagesUploaded)
async def upload_images(request: Request, db: Session = Depends(get_db)):
    """upload new image(s)
    todo: switch key in supplied plants list to id"""
    # the ui5 uploader control does somehow not work with the expected form/multipart format expected
    # via fastapi argument files = List[UploadFile] = File(...)
    # therefore, we directly go on the starlette request object
    form = await request.form()
    additional_data = json.loads(form.get('files-data'))
    # noinspection PyTypeChecker
Beispiel #20
0
            if event.images:
                for image in event.images:
                    image_obj = db.query(Image).filter(
                        Image.relative_path == image.path_original).first()

                    # not assigned to any event, yet
                    if not image_obj:
                        image_obj = Image(relative_path=image.path_original)
                        new_list.append(image_obj)

                    # not assigned to that specific event, yet
                    if image_obj not in event_obj.images:
                        event_obj.images.append(image_obj)

    if new_list:
        db.add_all(new_list)
    db.commit()

    logger.info(' Saving Events: ' + (description := ', '.join(
        [f'{key}: {counts[key]}' for key in counts.keys()])))
    results = {
        'action':
        'Saved events',
        'resource':
        'EventResource',
        'message':
        get_message(f'Updated events in database.', description=description)
    }

    return results