Beispiel #1
0
def _generate_thumbnail_name(instance: Union[Layer, Map]) -> Optional[str]:
    """
    Method returning file name for the thumbnail.
    If provided instance is a Map, and doesn't have any defined layers, None is returned.

    :param instance: instance of Layer or Map models
    :return: file name for the thumbnail
    :raises ThumbnailError: if provided instance is neither an instance of the Map nor of the Layer
    """

    if isinstance(instance, Layer):
        file_name = f"layer-{instance.uuid}-thumb.png"

    elif isinstance(instance, Map):
        # if a Map is empty - nothing to do here
        if not instance.layers:
            logger.debug(f"Thumbnail generation skipped - Map {instance.title} has no defined layers")
            return None

        file_name = f"map-{instance.uuid}-thumb.png"
    else:
        raise ThumbnailError(
            "Thumbnail generation didn't recognize the provided instance: it's neither a Layer nor a Map."
        )

    return file_name
Beispiel #2
0
def _generate_thumbnail_name(
        instance: Union[Dataset, Map, Document, GeoApp]) -> Optional[str]:
    """
    Method returning file name for the thumbnail.
    If provided instance is a Map, and doesn't have any defined datasets, None is returned.

    :param instance: instance of Dataset or Map models
    :return: file name for the thumbnail
    :raises ThumbnailError: if provided instance is neither an instance of the Map nor of the Dataset
    """

    if isinstance(instance, Dataset):
        file_name = f"dataset-{instance.uuid}-thumb.png"

    elif isinstance(instance, Map):
        # if a Map is empty - nothing to do here
        if not instance.maplayers:
            logger.debug(
                f"Thumbnail generation skipped - Map {instance.title} has no defined datasets"
            )
            return None

        file_name = f"map-{instance.uuid}-thumb.png"

    elif isinstance(instance, Document):
        file_name = f"document-{instance.uuid}-thumb.png"

    elif isinstance(instance, GeoApp):
        file_name = f"geoapp-{instance.uuid}-thumb.png"
    else:
        raise ThumbnailError(
            "Thumbnail generation didn't recognize the provided instance.")

    return file_name
Beispiel #3
0
def clean_bbox(bbox, target_crs):
    # make sure BBOX is provided with the CRS in a correct format
    source_crs = bbox[-1]

    srid_regex = re.match(r"EPSG:\d+", source_crs)
    if not srid_regex:
        logger.error(f"Thumbnail bbox is in a wrong format: {bbox}")
        raise ThumbnailError("Wrong BBOX format")

    # for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS;
    # if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with
    # the provided bbox is impossible.
    if target_crs == 'EPSG:3857' and bbox[-1].upper() != 'EPSG:3857':
        bbox = crop_to_3857_area_of_use(bbox)

    bbox = transform_bbox(bbox, target_crs=target_crs)
    return bbox
Beispiel #4
0
    def fetch(self, bbox: typing.List, zoom: int = None, *args, **kwargs):
        """
        The function fetching tiles from a Slippy Map provider, composing them into a single image, and cropping it
        to match the given BBOX. Retrieval of each tile is repeated self.max_retries times, waiting self.retry_delay
        seconds between consecutive requests.

        :param bbox: bounding box of the background image, dataset compliant format: [west, east, south, north, CRS]
        :param zoom: zoom with which to retrieve Slippy Map's tiles (by default, it's calculated based on width, height)
        :return: None if the CRS is different from self.tiles_crs, or background Image
        """

        if not self.url:
            logger.error("Thumbnail background requires url to be configured.")
            raise ThumbnailError("Tiled background improperly configured.")

        if bbox[-1].lower() != self.crs.lower():
            # background service is not available the requested CRS CRS
            logger.debug(
                f"Thumbnail background generation skipped. "
                f"Clashing CRSs: requested {bbox[-1]}, supported {self.crs}")
            return

        bbox = [float(coord) for coord in bbox[0:4]]

        # check if BBOX fits within the EPSG:3857 map, if not - return an empty background
        if bbox[2] > self._epsg3857_max_y or bbox[3] < -self._epsg3857_max_y:
            return Image.new("RGB",
                             (self.thumbnail_width, self.thumbnail_height),
                             (250, 250, 250))

        bbox4326 = self.bbox3857to4326(*bbox)

        # change bbox from dataset (left, right, bottom, top) to mercantile (left, bottom, right, top)
        self._mercantile_bbox = [
            bbox4326[0], bbox4326[2], bbox4326[1], bbox4326[3]
        ]

        # calculate zoom level
        if zoom is None:
            zoom = self.calculate_zoom()
        else:
            zoom = int(zoom)

        top_left_tile = mercantile.tile(bbox4326[0], bbox4326[3], zoom)
        bottom_right_tile = mercantile.tile(bbox4326[1], bbox4326[2], zoom)

        # rescaling factors - indicators of how west and east BBOX boundaries are offset in respect to the world's map;
        # east and west boundaries may exceed the maximum coordinate of the world in EPSG:3857. In such case additinal
        # number of tiles need to be fetched to compose the image and the boundary tiles' coordinates need to be
        # rescaled to ensure the proper image cropping.
        epsg3857_world_width = 2 * self._epsg3857_max_x

        west_rescaling_factor = 0
        if abs(bbox[0]) > self._epsg3857_max_x:
            west_rescaling_factor = ceil(
                (abs(bbox[0]) - self._epsg3857_max_x) /
                epsg3857_world_width) * copysign(1, bbox[0])

        east_rescaling_factor = 0
        if abs(bbox[1]) > self._epsg3857_max_x:
            east_rescaling_factor = ceil(
                (abs(bbox[1]) - self._epsg3857_max_x) /
                epsg3857_world_width) * copysign(1, bbox[1])

        map_row_tiles = 2**zoom - 1  # number of tiles in the Map's row for a certain zoom level

        map_worlds = int(east_rescaling_factor -
                         west_rescaling_factor)  # number maps in an image
        worlds_between = map_worlds - 1  # number of full maps in an image
        if top_left_tile.x > bottom_right_tile.x or bbox[1] - bbox[
                0] > epsg3857_world_width or map_worlds > 0:
            # BBOX crosses Slippy Map's border
            if worlds_between > 0:
                tiles_rows = (list(range(top_left_tile.x, map_row_tiles + 1)) +
                              worlds_between * list(range(map_row_tiles + 1)) +
                              list(range(bottom_right_tile.x + 1)))
            else:
                tiles_rows = list(range(top_left_tile.x,
                                        map_row_tiles + 1)) + list(
                                            range(bottom_right_tile.x + 1))
        else:
            # BBOx is contained by the Slippy Map
            if worlds_between > 0:
                tiles_rows = list(
                    range(top_left_tile.x, bottom_right_tile.x +
                          1)) + worlds_between * list(range(map_row_tiles + 1))
            else:
                tiles_rows = list(
                    range(top_left_tile.x, bottom_right_tile.x + 1))

        tiles_cols = list(range(top_left_tile.y, bottom_right_tile.y + 1))

        # if latitude boundaries extend world's height - add background's height, and set constant Y offset for tiles
        additional_height = 0
        fixed_top_offset = 0
        fixed_bottom_offset = 0

        north_extension3857 = max(0, bbox[3] - self._epsg3857_max_y)
        south_extension3857 = abs(min(0, bbox[2] + self._epsg3857_max_y))
        extension3857 = north_extension3857 + south_extension3857

        if extension3857:
            # get single tile's height in ESPG:3857
            tile_bounds = mercantile.bounds(tiles_rows[0], tiles_cols[0], zoom)
            _, south = self.point4326to3857(getattr(tile_bounds, "west"),
                                            getattr(tile_bounds, "south"))
            _, north = self.point4326to3857(getattr(tile_bounds, "west"),
                                            getattr(tile_bounds, "north"))
            tile_hight3857 = north - south

            additional_height = round(
                self.tile_size * extension3857 /
                tile_hight3857)  # based on linear proportion

            if north_extension3857:
                fixed_top_offset = round(self.tile_size * north_extension3857 /
                                         tile_hight3857)

            if south_extension3857:
                fixed_bottom_offset = round(
                    self.tile_size * south_extension3857 / tile_hight3857)

        background = Image.new(
            "RGB",
            (len(tiles_rows) * self.tile_size,
             len(tiles_cols) * self.tile_size + additional_height),
            (250, 250, 250),
        )

        for offset_x, x in enumerate(tiles_rows):
            for offset_y, y in enumerate(tiles_cols):
                if self.tms:
                    y = (2**zoom) - y - 1
                imgurl = self.url.format(x=x, y=y, z=zoom)

                im = None
                for retries in range(self.max_retries):
                    try:
                        resp, content = http_client.request(imgurl)
                        if resp.status_code > 400:
                            retries = self.max_retries - 1
                            raise Exception(f"{strip_tags(content)}")
                        im = BytesIO(content)
                        Image.open(
                            im).verify()  # verify that it is, in fact an image
                        break
                    except Exception as e:
                        logger.error(
                            f"Thumbnail background fetching from {imgurl} failed {retries} time(s) with: {e}"
                        )
                        if retries + 1 == self.max_retries:
                            raise e
                        time.sleep(self.retry_delay)
                        continue

                if im:
                    image = Image.open(
                        im
                    )  # "re-open" the file (required after running verify method)

                    # add the fetched tile to the background image, placing it under proper coordinates
                    background.paste(
                        image, (offset_x * self.tile_size,
                                offset_y * self.tile_size + fixed_top_offset))

        # get BBOX of the tiles
        top_left_bounds = mercantile.bounds(top_left_tile)
        bottom_right_bounds = mercantile.bounds(bottom_right_tile)

        tiles_bbox3857 = self.bbox4326to3857(
            getattr(top_left_bounds, "west"),
            getattr(bottom_right_bounds, "east"),
            getattr(bottom_right_bounds, "south"),
            getattr(top_left_bounds, "north"),
        )

        # rescale tiles' boundaries - if space covered by the input BBOX extends the width of the world,
        # (e.g. two "worlds" are present on the map), translation between tiles' BBOX and image's pixel requires
        # additional rescaling, for tiles' BBOX coordinates to match input BBOX coordinates
        west_coord = tiles_bbox3857[
            0] + west_rescaling_factor * epsg3857_world_width
        east_coord = tiles_bbox3857[
            1] + east_rescaling_factor * epsg3857_world_width

        # prepare translating function from received BBOX to pixel values of the background image
        src_quad = (0, fixed_top_offset, background.size[0],
                    background.size[1] - fixed_bottom_offset)
        to_src_px = utils.make_bbox_to_pixels_transf(
            [west_coord, tiles_bbox3857[2], east_coord, tiles_bbox3857[3]],
            src_quad)

        # translate received BBOX to pixel values
        minx, miny = to_src_px(bbox[0], bbox[2])
        maxx, maxy = to_src_px(bbox[1], bbox[3])

        # max and min function for Y axis were introduced to mitigate rounding errors
        crop_box = (
            ceil(minx),
            max(ceil(maxy) + fixed_top_offset, 0),
            floor(maxx),
            min(floor(miny) + fixed_top_offset, background.size[1]),
        )

        if not all([
                0 <= crop_x <= background.size[0]
                for crop_x in [crop_box[0], crop_box[2]]
        ]):
            raise ThumbnailError(
                f"Tiled background cropping error. Boundaries outside of the image: {crop_box}"
            )

        # crop background image to the desired bbox and resize it
        background = background.crop(box=crop_box)
        background = background.resize(
            (self.thumbnail_width, self.thumbnail_height))

        if sum(background.convert("L").getextrema()) in (0, 2):
            # either all black or all white
            logger.error("Thumbnail background outside the allowed area.")
            raise ThumbnailError(
                "Thumbnail background outside the allowed area.")
        return background
Beispiel #5
0
def create_thumbnail(
    instance: Union[Layer, Map],
    wms_version: str = settings.OGC_SERVER["default"].get("WMS_VERSION", "1.1.1"),
    bbox: Optional[Union[List, Tuple]] = None,
    forced_crs: Optional[str] = None,
    styles: Optional[List] = None,
    overwrite: bool = False,
    background_zoom: Optional[int] = None,
) -> None:
    """
    Function generating and saving a thumbnail of the given instance (Layer or Map), which is composed of
    outcomes of WMS GetMap queries to the instance's layers providers, and an outcome of querying background
    provider for thumbnail's background (by default Slippy Map provider).

    :param instance: instance of Layer or Map models
    :param wms_version: WMS version of the query
    :param bbox: bounding box of the thumbnail in format: (west, east, south, north, CRS), where CRS is in format
                 "EPSG:XXXX"
    :param forced_crs: CRS which should be used to fetch data from WMS services in format "EPSG:XXXX". By default
                       all data is translated and retrieved in EPSG:3857, since this enables background fetching from
                       Slippy Maps providers. Forcing another CRS can cause skipping background generation in
                       the thumbnail
    :param styles: styles, which OGC server should use for rendering an image
    :param overwrite: overwrite existing thumbnail
    :param background_zoom: zoom of the XYZ Slippy Map used to retrieve background image,
                            if Slippy Map is used as background
    """

    instance.refresh_from_db()

    default_thumbnail_name = _generate_thumbnail_name(instance)
    mime_type = "image/png"
    width = settings.THUMBNAIL_SIZE["width"]
    height = settings.THUMBNAIL_SIZE["height"]

    if default_thumbnail_name is None:
        # instance is Map and has no layers defined
        utils.assign_missing_thumbnail(instance)
        return

    # handle custom, uploaded thumbnails, which may have different extensions from the default thumbnail
    thumbnail_exists = False
    if instance.thumbnail_url and instance.thumbnail_url != settings.MISSING_THUMBNAIL:
        thumbnail_exists = thumb_exists(instance.thumbnail_url.rsplit('/')[-1])

    if (thumbnail_exists or thumb_exists(default_thumbnail_name)) and not overwrite:
        logger.debug(f"Thumbnail for {instance.name} already exists. Skipping thumbnail generation.")
        return

    # --- determine target CRS and bbox ---
    target_crs = forced_crs.upper() if forced_crs is not None else "EPSG:3857"

    compute_bbox_from_layers = False
    if bbox:
        # make sure BBOX is provided with the CRS in a correct format
        source_crs = bbox[-1]

        srid_regex = re.match(r"EPSG:\d+", source_crs)
        if not srid_regex:
            logger.error(f"Thumbnail bbox is in a wrong format: {bbox}")
            raise ThumbnailError("Wrong BBOX format")

        # for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS;
        # if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with
        # the provided bbox is impossible.
        if target_crs == 'EPSG:3857' and bbox[-1].upper() != 'EPSG:3857':
            bbox = utils.crop_to_3857_area_of_use(bbox)

        bbox = utils.transform_bbox(bbox, target_crs=target_crs)
    else:
        compute_bbox_from_layers = True

    # --- define layer locations ---
    locations, layers_bbox = _layers_locations(instance, compute_bbox=compute_bbox_from_layers, target_crs=target_crs)

    if compute_bbox_from_layers:
        if not layers_bbox:
            raise ThumbnailError(f"Thumbnail generation couldn't determine a BBOX for: {instance}.")
        else:
            bbox = layers_bbox

    # --- expand the BBOX to match the set thumbnail's ratio (prevent thumbnail's distortions) ---
    bbox = utils.expand_bbox_to_ratio(bbox)

    # --- add default style ---
    if not styles and hasattr(instance, "default_style"):
        if instance.default_style:
            styles = [instance.default_style.name]

    # --- fetch WMS layers ---
    partial_thumbs = []

    for ogc_server, layers in locations:
        try:
            partial_thumbs.append(utils.get_map(
                ogc_server,
                layers,
                wms_version=wms_version,
                bbox=bbox,
                mime_type=mime_type,
                styles=styles,
                width=width,
                height=height,
            ))
        except Exception as e:
            logger.error(f"Exception occurred while fetching partial thumbnail for {instance.name}.")
            logger.exception(e)

    if not partial_thumbs:
        utils.assign_missing_thumbnail(instance)
        raise ThumbnailError("Thumbnail generation failed - no image retrieved from WMS services.")

    # --- merge retrieved WMS images ---
    merged_partial_thumbs = Image.new("RGBA", (width, height), (255, 255, 255, 0))

    for image in partial_thumbs:
        if image:
            content = BytesIO(image)
            try:
                img = Image.open(content)
                img.verify()  # verify that it is, in fact an image
                img = Image.open(BytesIO(image))  # "re-open" the file (required after running verify method)
                merged_partial_thumbs.paste(img, mask=img.convert('RGBA'))
            except UnidentifiedImageError as e:
                logger.error(f"Thumbnail generation. Error occurred while fetching layer image: {image}")
                logger.exception(e)

    # --- fetch background image ---
    try:
        BackgroundGenerator = import_string(settings.THUMBNAIL_BACKGROUND["class"])
        background = BackgroundGenerator(width, height).fetch(bbox, background_zoom)
    except Exception as e:
        logger.error(f"Thumbnail generation. Error occurred while fetching background image: {e}")
        logger.exception(e)
        background = None

    # --- overlay image with background ---
    thumbnail = Image.new("RGB", (width, height), (250, 250, 250))

    if background is not None:
        thumbnail.paste(background, (0, 0))

    thumbnail.paste(merged_partial_thumbs, (0, 0), merged_partial_thumbs)

    # convert image to the format required by save_thumbnail
    with BytesIO() as output:
        thumbnail.save(output, format="PNG")
        content = output.getvalue()

    # save thumbnail
    instance.save_thumbnail(default_thumbnail_name, image=content)
Beispiel #6
0
class BaseApiTests(APITestCase):

    fixtures = [
        'initial_data.json',
        'group_test_data.json',
        'default_oauth_apps.json',
        "test_thesaurus.json"
    ]

    def setUp(self):
        create_models(b'document')
        create_models(b'map')
        create_models(b'dataset')

    def test_gropus_list(self):
        """
        Ensure we can access the gropus list.
        """
        url = reverse('group-profiles-list')
        # Unauhtorized
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 403)

        # Auhtorized
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        logger.debug(response.data)
        self.assertEqual(response.data['total'], 2)
        self.assertEqual(len(response.data['group_profiles']), 2)

        url = reverse('group-profiles-detail', kwargs={'pk': 1})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        logger.debug(response.data)
        self.assertEqual(response.data['group_profile']['title'], 'Registered Members')
        self.assertEqual(response.data['group_profile']['description'], 'Registered Members')
        self.assertEqual(response.data['group_profile']['access'], 'private')
        self.assertEqual(response.data['group_profile']['group']['name'], response.data['group_profile']['slug'])

    def test_users_list(self):
        """
        Ensure we can access the users list.
        """
        url = reverse('users-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        logger.debug(response.data)
        self.assertEqual(response.data['total'], 0)
        self.assertEqual(len(response.data['users']), 0)

        # Auhtorized
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        logger.debug(response.data)
        self.assertEqual(response.data['total'], 10)
        self.assertEqual(len(response.data['users']), 10)
        # response has link to the response
        self.assertTrue('link' in response.data['users'][0].keys())

        url = reverse('users-detail', kwargs={'pk': 1})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        logger.debug(response.data)
        self.assertEqual(response.data['user']['username'], 'admin')
        self.assertIsNotNone(response.data['user']['avatar'])

        # anonymous users are not in contributors group
        url = reverse('users-detail', kwargs={'pk': -1})
        response = self.client.get(url, format='json')
        self.assertNotIn('add_resource', response.data['user']['perms'])

        # Bobby
        self.assertTrue(self.client.login(username='******', password='******'))
        # Bobby cannot access other users' details
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 404)

        # Bobby can see himself in the list
        url = reverse('users-list')
        self.assertEqual(len(response.data), 1)
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        logger.debug(response.data)
        self.assertEqual(response.data['total'], 1)
        self.assertEqual(len(response.data['users']), 1)

        # Bobby can access its own details
        bobby = get_user_model().objects.filter(username='******').get()
        url = reverse('users-detail', kwargs={'pk': bobby.id})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        logger.debug(response.data)
        self.assertEqual(response.data['user']['username'], 'bobby')
        self.assertIsNotNone(response.data['user']['avatar'])
        # default contributor group_perm is returned in perms
        self.assertIn('add_resource', response.data['user']['perms'])

    def test_register_users(self):
        """
        Ensure users are created with default groups.
        """
        url = reverse('users-list')
        user_data = {
            'username': '******',
        }
        response = self.client.post(url, data=user_data, format='json')
        self.assertEqual(response.status_code, 201)
        # default contributor group_perm is returned in perms
        self.assertIn('add_resource', response.data['user']['perms'])

    def test_base_resources(self):
        """
        Ensure we can access the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)
        logger.debug(response.data)

        # Remove public permissions to Layers
        from geonode.layers.utils import set_datasets_permissions
        set_datasets_permissions(
            "read",  # permissions_name
            None,  # resources_names == None (all layers)
            [get_anonymous_user()],  # users_usernames
            None,  # groups_names
            True,   # delete_flag
        )
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # response has link to the response
        self.assertTrue('link' in response.data['resources'][0].keys())
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)
        logger.debug(response.data)

        # Bobby
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)
        logger.debug(response.data)

        # Norman
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)
        logger.debug(response.data)

        # Pagination
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        response = self.client.get(f"{url}?page_size=17", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 17)

        # Check user permissions
        resource = ResourceBase.objects.filter(owner__username='******').first()
        # Admin
        response = self.client.get(f"{url}/{resource.id}/", format='json')
        self.assertEqual(response.data['resource']['state'], enumerations.STATE_PROCESSED)
        self.assertEqual(response.data['resource']['sourcetype'], enumerations.SOURCE_TYPE_LOCAL)
        self.assertTrue('change_resourcebase' in list(response.data['resource']['perms']))
        # Annonymous
        self.assertIsNone(self.client.logout())
        response = self.client.get(f"{url}/{resource.id}/", format='json')
        self.assertFalse('change_resourcebase' in list(response.data['resource']['perms']))
        # user owner
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(f"{url}/{resource.id}/", format='json')
        self.assertTrue('change_resourcebase' in list(response.data['resource']['perms']))
        # user not owner and not assigned
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(f"{url}/{resource.id}/", format='json')
        self.assertFalse('change_resourcebase' in list(response.data['resource']['perms']))

    def test_delete_user_with_resource(self):
        owner, created = get_user_model().objects.get_or_create(username='******')
        Dataset(
            title='Test Remove User',
            abstract='abstract',
            name='Test Remove User',
            alternate='Test Remove User',
            uuid=str(uuid4()),
            owner=owner,
            subtype='raster',
            category=TopicCategory.objects.get(identifier='elevation')
        ).save()
        # Delete user and check if default user is updated
        owner.delete()
        self.assertEqual(
            ResourceBase.objects.get(title='Test Remove User').owner,
            get_user_model().objects.get(username='******')
        )

    def test_search_resources(self):
        """
        Ensure we can search across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        response = self.client.get(
            f"{url}?search=ca&search_fields=title&search_fields=abstract", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 1)
        # Pagination
        self.assertEqual(len(response.data['resources']), 1)

    def test_filter_resources(self):
        """
        Ensure we can filter across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        # Filter by owner == bobby
        response = self.client.get(f"{url}?filter{{owner.username}}=bobby", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 3)
        # Pagination
        self.assertEqual(len(response.data['resources']), 3)

        # Filter by resource_type == document
        response = self.client.get(f"{url}?filter{{resource_type}}=document", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 9)
        # Pagination
        self.assertEqual(len(response.data['resources']), 9)

        # Filter by resource_type == layer and title like 'common morx'
        response = self.client.get(
            f"{url}?filter{{resource_type}}=dataset&filter{{title.icontains}}=common morx", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 1)
        # Pagination
        self.assertEqual(len(response.data['resources']), 1)

        # Filter by Keywords
        response = self.client.get(
            f"{url}?filter{{keywords.name}}=here", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 1)
        # Pagination
        self.assertEqual(len(response.data['resources']), 1)

        # Filter by Metadata Regions
        response = self.client.get(
            f"{url}?filter{{regions.name.icontains}}=Italy", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 0)
        # Pagination
        self.assertEqual(len(response.data['resources']), 0)

        # Filter by Metadata Categories
        response = self.client.get(
            f"{url}?filter{{category.identifier}}=elevation", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 6)
        # Pagination
        self.assertEqual(len(response.data['resources']), 6)

        # Extent Filter
        response = self.client.get(f"{url}?page_size=26&extent=-180,-90,180,90", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 26)

        response = self.client.get(f"{url}?page_size=26&extent=0,0,100,100", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 26)

        response = self.client.get(f"{url}?page_size=26&extent=-10,-10,-1,-1", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 12)
        # Pagination
        self.assertEqual(len(response.data['resources']), 12)

        # Extent Filter: Crossing Dateline
        extent = "-180.0000,56.9689,-162.5977,70.7435,155.9180,56.9689,180.0000,70.7435"
        response = self.client.get(
            f"{url}?page_size=26&extent={extent}", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 12)
        # Pagination
        self.assertEqual(len(response.data['resources']), 12)

    def test_sort_resources(self):
        """
        Ensure we can sort the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        response = self.client.get(
            f"{url}?sort[]=title", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)

        resource_titles = []
        for _r in response.data['resources']:
            resource_titles.append(_r['title'])
        sorted_resource_titles = sorted(resource_titles.copy())
        self.assertEqual(resource_titles, sorted_resource_titles)

        response = self.client.get(
            f"{url}?sort[]=-title", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 26)
        # Pagination
        self.assertEqual(len(response.data['resources']), 10)

        resource_titles = []
        for _r in response.data['resources']:
            resource_titles.append(_r['title'])

        reversed_resource_titles = sorted(resource_titles.copy())
        self.assertNotEqual(resource_titles, reversed_resource_titles)

    def test_perms_resources(self):
        """
        Ensure we can Get & Set Permissions across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        resource = ResourceBase.objects.filter(owner__username='******').first()
        set_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", 'set_perms/')
        get_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", 'get_perms/')

        url = reverse('base-resources-detail', kwargs={'pk': resource.pk})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(int(response.data['resource']['pk']), int(resource.pk))

        response = self.client.get(get_perms_url, format='json')
        self.assertEqual(response.status_code, 200)
        resource_perm_spec = response.data
        self.assertTrue('bobby' in resource_perm_spec['users'])
        self.assertFalse('norman' in resource_perm_spec['users'])

        # Add perms to Norman
        resource_perm_spec['users']['norman'] = resource_perm_spec['users']['bobby']
        response = self.client.put(set_perms_url, data=resource_perm_spec, format='json')
        self.assertEqual(response.status_code, 200)

        response = self.client.get(get_perms_url, format='json')
        self.assertEqual(response.status_code, 200)
        resource_perm_spec = response.data
        self.assertTrue('norman' in resource_perm_spec['users'])

        # Remove perms to Norman
        resource_perm_spec['users']['norman'] = []
        response = self.client.put(set_perms_url, data=resource_perm_spec, format='json')
        self.assertEqual(response.status_code, 200)

        response = self.client.get(get_perms_url, format='json')
        self.assertEqual(response.status_code, 200)
        resource_perm_spec = response.data
        self.assertFalse('norman' in resource_perm_spec['users'])

        # Ensure get_perms and set_perms are done by users with correct permissions.
        # logout admin user
        self.assertIsNone(self.client.logout())
        # get perms
        response = self.client.get(get_perms_url, format='json')
        self.assertEqual(response.status_code, 403)
        # set perms
        response = self.client.put(set_perms_url, data=resource_perm_spec, format='json')
        self.assertEqual(response.status_code, 403)
        # login resourse owner
        # get perms
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(get_perms_url, format='json')
        self.assertEqual(response.status_code, 200)
        # set perms
        response = self.client.put(set_perms_url, data=resource_perm_spec, format='json')
        self.assertEqual(response.status_code, 200)

    def test_featured_and_published_resources(self):
        """
        Ensure we can Get & Set Permissions across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        resources = ResourceBase.objects.filter(owner__username='******', metadata_only=False)

        url = urljoin(f"{reverse('base-resources-list')}/", 'featured/')
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], 0)
        # Pagination
        self.assertEqual(len(response.data['resources']), 0)

        resources.filter(resource_type='map').update(featured=True)
        url = urljoin(f"{reverse('base-resources-list')}/", 'featured/')
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        self.assertEqual(response.data['total'], resources.filter(resource_type='map').count())
        # Pagination
        self.assertEqual(len(response.data['resources']), resources.filter(resource_type='map').count())

    def test_resource_types(self):
        """
        Ensure we can Get & Set Permissions across the Resource Base list.
        """
        url = urljoin(f"{reverse('base-resources-list')}/", 'resource_types/')
        response = self.client.get(url, format='json')
        r_type_names = [item['name'] for item in response.data['resource_types']]
        self.assertEqual(response.status_code, 200)
        self.assertTrue('resource_types' in response.data)
        self.assertTrue('dataset' in r_type_names)
        self.assertTrue('map' in r_type_names)
        self.assertTrue('document' in r_type_names)
        self.assertFalse('service' in r_type_names)

    def test_get_favorites(self):
        """
        Ensure we get user's favorite resources.
        """
        dataset = Dataset.objects.first()
        url = urljoin(f"{reverse('base-resources-list')}/", 'favorites/')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 403)
        # Authenticated user
        bobby = get_user_model().objects.get(username='******')
        self.assertTrue(self.client.login(username='******', password='******'))
        favorite = Favorite.objects.create_favorite(dataset, bobby)
        response = self.client.get(url, format='json')
        self.assertEqual(response.data['total'], 1)
        self.assertEqual(response.status_code, 200)
        # clean up
        favorite.delete()

    def test_create_and_delete_favorites(self):
        """
        Ensure we can add and remove resources to user's favorite.
        """
        dataset = get_resources_with_perms(get_user_model().objects.get(pk=-1)).first()
        url = urljoin(f"{reverse('base-resources-list')}/", f"{dataset.pk}/favorite/")
        # Anonymous
        response = self.client.post(url, format='json')
        self.assertEqual(response.status_code, 403)
        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.post(url, format="json")
        self.assertEqual(response.data["message"], "Successfuly added resource to favorites")
        self.assertEqual(response.status_code, 201)
        # add resource to favorite again
        response = self.client.post(url, format="json")
        self.assertEqual(response.data["message"], "Resource is already in favorites")
        self.assertEqual(response.status_code, 400)
        # remove resource from favorites
        response = self.client.delete(url, format="json")
        self.assertEqual(response.data["message"], "Successfuly removed resource from favorites")
        self.assertEqual(response.status_code, 200)
        # remove resource to favorite again
        response = self.client.delete(url, format="json")
        self.assertEqual(response.data["message"], "Resource not in favorites")
        self.assertEqual(response.status_code, 404)

    def test_search_resources_with_favorite_true_and_no_favorite_should_return_0(self):
        """
        Ensure we can search across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        response = self.client.get(
            f"{url}?favorite=true", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        # No favorite are saved, so the total should be 0
        self.assertEqual(response.data['total'], 0)
        self.assertEqual(len(response.data['resources']), 0)

    def test_search_resources_with_favorite_true_and_favorite_should_return_1(self):
        """
        Ensure we can search across the Resource Base list.
        """
        url = reverse('base-resources-list')
        # Admin
        admin = get_user_model().objects.get(username='******')
        dataset = Dataset.objects.first()
        Favorite.objects.create_favorite(dataset, admin)

        self.assertTrue(self.client.login(username='******', password='******'))

        response = self.client.get(
            f"{url}?favorite=true", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data), 5)
        # 1 favorite is saved, so the total should be 1
        self.assertEqual(response.data['total'], 1)
        self.assertEqual(len(response.data['resources']), 1)

    @patch('PIL.Image.open', return_value=test_image)
    def test_thumbnail_urls(self, img):
        """
        Ensure the thumbnail url reflects the current active Thumb on the resource.
        """
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        resource = ResourceBase.objects.filter(owner__username='******').first()
        url = reverse('base-resources-detail', kwargs={'pk': resource.pk})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(int(response.data['resource']['pk']), int(resource.pk))
        thumbnail_url = response.data['resource']['thumbnail_url']
        self.assertIsNone(thumbnail_url)

        f = BytesIO(test_image.tobytes())
        f.name = 'test_image.jpeg'
        curated_thumbnail = CuratedThumbnail.objects.create(resource=resource, img=File(f))

        url = reverse('base-resources-detail', kwargs={'pk': resource.pk})
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(int(response.data['resource']['pk']), int(resource.pk))
        thumbnail_url = response.data['resource']['thumbnail_url']
        self.assertTrue(curated_thumbnail.thumbnail_url in thumbnail_url)

    def test_embed_urls(self):
        """
        Ensure the embed urls reflect the concrete instance ones.
        """
        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))

        resources = ResourceBase.objects.all()
        for resource in resources:
            url = reverse('base-resources-detail', kwargs={'pk': resource.pk})
            response = self.client.get(url, format='json')
            if resource.title.endswith('metadata true'):
                self.assertEqual(response.status_code, 404)
            else:
                self.assertEqual(response.status_code, 200)
                self.assertEqual(int(response.data['resource']['pk']), int(resource.pk))
                embed_url = response.data['resource']['embed_url']
                self.assertIsNotNone(embed_url)

                instance = resource.get_real_instance()
                if hasattr(instance, 'embed_url'):
                    if instance.embed_url != NotImplemented:
                        self.assertEqual(build_absolute_uri(instance.embed_url), embed_url)
                    else:
                        self.assertEqual("", embed_url)

    def test_owners_list(self):
        """
        Ensure we can access the list of owners.
        """
        url = reverse('owners-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], 8)

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], 8)
        response = self.client.get(f"{url}?type=geoapp", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], 0)
        response = self.client.get(f"{url}?type=dataset&title__icontains=CA", format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], 1)
        # response has link to the response
        self.assertTrue('link' in response.data['owners'][0].keys())

        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], 8)

    def test_categories_list(self):
        """
        Ensure we can access the list of categories.
        """
        url = reverse('categories-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], TopicCategory.objects.count())

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], TopicCategory.objects.count())
        # response has link to the response
        self.assertTrue('link' in response.data['categories'][0].keys())

        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], TopicCategory.objects.count())

    def test_regions_list(self):
        """
        Ensure we can access the list of regions.
        """
        url = reverse('regions-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], Region.objects.count())

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], Region.objects.count())
        # response has link to the response
        self.assertTrue('link' in response.data['regions'][0].keys())

        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], Region.objects.count())

    def test_keywords_list(self):
        """
        Ensure we can access the list of keywords.
        """
        url = reverse('keywords-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], HierarchicalKeyword.objects.count())

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], HierarchicalKeyword.objects.count())
        # response has link to the response
        self.assertTrue('link' in response.data['keywords'][0].keys())

        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], HierarchicalKeyword.objects.count())

    def test_tkeywords_list(self):
        """
        Ensure we can access the list of thasaurus keywords.
        """
        url = reverse('tkeywords-list')
        # Anonymous
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], ThesaurusKeyword.objects.count())

        # Admin
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], ThesaurusKeyword.objects.count())
        # response has link to the response
        self.assertTrue('link' in response.data['tkeywords'][0].keys())

        # Authenticated user
        self.assertTrue(self.client.login(username='******', password='******'))
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['total'], ThesaurusKeyword.objects.count())

    def test_set_thumbnail_from_bbox_from_Anonymous_user_raise_permission_error(self):
        """
        Given a request with Anonymous user, should raise an authentication error.
        """
        dataset_id = sys.maxsize
        url = reverse('base-resources-set-thumb-from-bbox', args=[dataset_id])
        # Anonymous
        expected = {
            "detail": "Authentication credentials were not provided."
        }
        response = self.client.post(url, format='json')
        self.assertEqual(response.status_code, 403)
        self.assertEqual(expected, response.json())

    @patch("geonode.base.api.views.create_thumbnail")
    def test_set_thumbnail_from_bbox_from_logged_user_for_existing_dataset(self, mock_create_thumbnail):
        """
        Given a logged User and an existing dataset, should create the expected thumbnail url.
        """
        mock_create_thumbnail.return_value = "http://*****:*****@patch("geonode.base.api.views.create_thumbnail", side_effect=ThumbnailError('Some exception during thumb creation'))
    def test_set_thumbnail_from_bbox_from_logged_user_for_existing_dataset_raise_exp(self, mock_exp):
        """
        Given a logged User and an existing dataset, should raise a ThumbnailException.
        """
        # Admin
        self.client.login(username="******", password="******")
        dataset_id = Dataset.objects.first().resourcebase_ptr_id
        url = reverse('base-resources-set-thumb-from-bbox', args=[dataset_id])
        payload = {
            "bbox": [],
            "srid": "EPSG:3857"
        }
        response = self.client.post(url, data=payload, format='json')

        expected = {
            "message": "Some exception during thumb creation"
        }
        self.assertEqual(response.status_code, 500)
        self.assertEqual(expected, response.json())
Beispiel #7
0
def get_map(
        ogc_server_location: str,
        layers: List,
        bbox: List,
        wms_version: str = settings.OGC_SERVER["default"].get("WMS_VERSION", "1.1.1"),
        mime_type: str = "image/png",
        styles: List = None,
        width: int = 240,
        height: int = 200,
        max_retries: int = 3,
        retry_delay: int = 1,
):
    """
    Function fetching an image from OGC server.
    For the requests to the configured OGC backend (ogc_server_settings.LOCATION) the function tries to generate
    an access_token and attach it to the URL.
    If access_token is not added ant the request is against Geoserver Basic Authentication is used instead.
    If image retrieval fails, function retries to fetch the image max_retries times, waiting
    retry_delay seconds between consecutive requests.

    :param ogc_server_location: OGC server URL
    :param layers: layers which should be fetched from the OGC server
    :param bbox: area's bounding box in format: [west, east, south, north, CRS]
    :param wms_version: WMS version of the query (default: 1.1.1)
    :param mime_type: mime type of the returned image
    :param styles: styles, which OGC server should use for rendering an image
    :param width: width of the returned image
    :param height: height of the returned image
    :param max_retries: maximum number of retries before skipping retrieval
    :param retry_delay: number of seconds waited between retries
    :returns: retrieved image
    """

    ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"]

    if ogc_server_location is not None:
        thumbnail_url = ogc_server_location
    else:
        thumbnail_url = ogc_server_settings.LOCATION

    if thumbnail_url.startswith(ogc_server_settings.PUBLIC_LOCATION):
        thumbnail_url = thumbnail_url.replace(ogc_server_settings.PUBLIC_LOCATION, ogc_server_settings.LOCATION)

    wms_endpoint = ""
    additional_kwargs = {}
    if thumbnail_url == ogc_server_settings.LOCATION:
        # add access token to requests to Geoserver (logic based on the previous implementation)
        username = ogc_server_settings.credentials.username
        user = get_user_model().objects.filter(username=username).first()
        if user:
            access_token = get_or_create_token(user)
            if access_token and not access_token.is_expired():
                additional_kwargs['access_token'] = access_token.token

        # add WMS endpoint to requests to Geoserver
        wms_endpoint = getattr(ogc_server_settings, "WMS_ENDPOINT") or "ows"

    # prepare authorization for WMS service
    headers = {}
    if thumbnail_url.startswith(ogc_server_settings.LOCATION):
        if "access_token" not in additional_kwargs.keys():
            # for the Geoserver backend, use Basic Auth, if access_token is not provided
            _user, _pwd = ogc_server_settings.credentials
            encoded_credentials = base64.b64encode(f"{_user}:{_pwd}".encode("UTF-8")).decode("ascii")
            headers["Authorization"] = f"Basic {encoded_credentials}"
        else:
            headers["Authorization"] = f"Bearer {additional_kwargs['access_token']}"

    wms = WebMapService(
        f"{thumbnail_url}{wms_endpoint}",
        version=wms_version,
        headers=headers)

    image = None
    for retry in range(max_retries):
        try:
            # fetch data
            image = wms.getmap(
                layers=layers,
                styles=styles,
                srs=bbox[-1] if bbox else None,
                bbox=[bbox[0], bbox[2], bbox[1], bbox[3]] if bbox else None,
                size=(width, height),
                format=mime_type,
                transparent=True,
                timeout=getattr(ogc_server_settings, "TIMEOUT", None),
                **additional_kwargs,
            )

            # validate response
            if not image or "ServiceException" in str(image.read()):
                raise ThumbnailError(
                    f"Fetching partial thumbnail from {thumbnail_url} failed with response: {str(image)}"
                )

        except Exception as e:
            if retry + 1 >= max_retries:
                logger.exception(e)
                return

            time.sleep(retry_delay)
            continue
        else:
            break

    return image.read()
Beispiel #8
0
def create_thumbnail(
    instance: Union[Dataset, Map],
    wms_version: str = settings.OGC_SERVER["default"].get("WMS_VERSION", "1.1.1"),
    bbox: Optional[Union[List, Tuple]] = None,
    forced_crs: Optional[str] = None,
    styles: Optional[List] = None,
    overwrite: bool = False,
    background_zoom: Optional[int] = None,
) -> None:
    """
    Function generating and saving a thumbnail of the given instance (Dataset or Map), which is composed of
    outcomes of WMS GetMap queries to the instance's datasets providers, and an outcome of querying background
    provider for thumbnail's background (by default Slippy Map provider).

    :param instance: instance of Dataset or Map models
    :param wms_version: WMS version of the query
    :param bbox: bounding box of the thumbnail in format: (west, east, south, north, CRS), where CRS is in format
                 "EPSG:XXXX"
    :param forced_crs: CRS which should be used to fetch data from WMS services in format "EPSG:XXXX". By default
                       all data is translated and retrieved in EPSG:3857, since this enables background fetching from
                       Slippy Maps providers. Forcing another CRS can cause skipping background generation in
                       the thumbnail
    :param styles: styles, which OGC server should use for rendering an image
    :param overwrite: overwrite existing thumbnail
    :param background_zoom: zoom of the XYZ Slippy Map used to retrieve background image,
                            if Slippy Map is used as background
    """

    instance.refresh_from_db()

    default_thumbnail_name = _generate_thumbnail_name(instance)
    mime_type = "image/png"
    width = settings.THUMBNAIL_SIZE["width"]
    height = settings.THUMBNAIL_SIZE["height"]

    if default_thumbnail_name is None:
        # instance is Map and has no datasets defined
        utils.assign_missing_thumbnail(instance)
        return

    # handle custom, uploaded thumbnails, which may have different extensions from the default thumbnail
    thumbnail_exists = False
    if instance.thumbnail_url and instance.thumbnail_url != static(utils.MISSING_THUMB):
        thumbnail_exists = utils.thumb_exists(instance.thumbnail_url.rsplit('/')[-1])

    if (thumbnail_exists or utils.thumb_exists(default_thumbnail_name)) and not overwrite:
        logger.debug(f"Thumbnail for {instance.name} already exists. Skipping thumbnail generation.")
        return

    # --- determine target CRS and bbox ---
    target_crs = forced_crs.upper() if forced_crs is not None else "EPSG:3857"

    compute_bbox_from_datasets = False
    is_map_with_datasets = True

    if isinstance(instance, Map):
        is_map_with_datasets = MapLayer.objects.filter(map=instance, local=True).exclude(dataset=None).count() > 0
    if bbox:
        bbox = utils.clean_bbox(bbox, target_crs)
    elif instance.ll_bbox_polygon:
        _bbox = BBOXHelper(instance.ll_bbox_polygon.extent)
        srid = instance.ll_bbox_polygon.srid
        bbox = [_bbox.xmin, _bbox.xmax, _bbox.ymin, _bbox.ymax, f"EPSG:{srid}"]
        bbox = utils.clean_bbox(bbox, target_crs)
    else:
        compute_bbox_from_datasets = True
    # --- define dataset locations ---
    locations, datasets_bbox = _datasets_locations(instance, compute_bbox=compute_bbox_from_datasets, target_crs=target_crs)

    if compute_bbox_from_datasets and is_map_with_datasets:
        if not datasets_bbox:
            raise ThumbnailError(f"Thumbnail generation couldn't determine a BBOX for: {instance}.")
        else:
            bbox = datasets_bbox

    # --- expand the BBOX to match the set thumbnail's ratio (prevent thumbnail's distortions) ---
    bbox = utils.expand_bbox_to_ratio(bbox) if bbox else None

    # --- add default style ---
    if not styles and hasattr(instance, "default_style"):
        if instance.default_style:
            styles = [instance.default_style.name]

    # --- fetch WMS datasets ---
    partial_thumbs = []

    for ogc_server, datasets, _styles in locations:
        if isinstance(instance, Map) and len(datasets) == len(_styles):
            styles = _styles
        try:
            partial_thumbs.append(
                utils.get_map(
                    ogc_server,
                    datasets,
                    wms_version=wms_version,
                    bbox=bbox,
                    mime_type=mime_type,
                    styles=styles,
                    width=width,
                    height=height,
                )
            )
        except Exception as e:
            logger.error(f"Exception occurred while fetching partial thumbnail for {instance.title}.")
            logger.exception(e)

    if not partial_thumbs and is_map_with_datasets:
        utils.assign_missing_thumbnail(instance)
        raise ThumbnailError("Thumbnail generation failed - no image retrieved from WMS services.")

    # --- merge retrieved WMS images ---
    merged_partial_thumbs = Image.new("RGBA", (width, height), (255, 255, 255, 0))

    for image in partial_thumbs:
        if image:
            content = BytesIO(image)
            try:
                img = Image.open(content)
                img.verify()  # verify that it is, in fact an image
                img = Image.open(BytesIO(image))  # "re-open" the file (required after running verify method)
                merged_partial_thumbs.paste(img, mask=img.convert('RGBA'))
            except UnidentifiedImageError as e:
                logger.error(f"Thumbnail generation. Error occurred while fetching dataset image: {image}")
                logger.exception(e)

    # --- fetch background image ---
    try:
        BackgroundGenerator = import_string(settings.THUMBNAIL_BACKGROUND["class"])
        background = BackgroundGenerator(width, height).fetch(bbox, background_zoom) if bbox else None
    except Exception as e:
        logger.error(f"Thumbnail generation. Error occurred while fetching background image: {e}")
        logger.exception(e)
        background = None

    # --- overlay image with background ---
    thumbnail = Image.new("RGB", (width, height), (250, 250, 250))

    if background is not None:
        thumbnail.paste(background, (0, 0))

    thumbnail.paste(merged_partial_thumbs, (0, 0), merged_partial_thumbs)

    # convert image to the format required by save_thumbnail
    with BytesIO() as output:
        thumbnail.save(output, format="PNG")
        content = output.getvalue()

    # save thumbnail
    instance.save_thumbnail(default_thumbnail_name, image=content)
    return instance.thumbnail_url