Exemplo n.º 1
0
    def synchronize(self, with_user=None):
        """Synchronizer le jeu de données avec l'instance de CKAN."""
        # 'with_user' n'est pas utiliser dans ce contexte

        # Définition des propriétés de la « ressource »
        # =============================================

        id = self.name
        name = 'Aperçu cartographique'.format(name=self.resource.title)
        description = self.resource.description
        organisation = self.resource.dataset.organisation

        base_url = OWS_URL_PATTERN.format(
            organisation=organisation.slug).replace('?', '')

        getlegendgraphic = (
            '{base_url}?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic'
            '&LAYER={layer}&FORMAT=image/png').format(base_url=base_url,
                                                      layer=id)

        api = {
            'url': base_url,
            'typename': id,
            'getlegendgraphic': getlegendgraphic
        }

        try:
            DEFAULT_SRID = settings.DEFAULTS_VALUES['SRID']
        except Exception:
            DEFAULT_SRID = 4326
        else:
            SupportedCrs = apps.get_model(app_label='idgo_admin',
                                          model_name='SupportedCrs')
            try:
                SupportedCrs.objects.get(auth_name='EPSG',
                                         auth_code=DEFAULT_SRID)
            except SupportedCrs.DoesNotExist:
                DEFAULT_SRID = 4326

        if self.type == 'vector':
            if self.resource.format_type.extension.lower() in ('json',
                                                               'geojson'):
                outputformat = 'shapezip'  # Il faudrait être sûr que le format existe avec le même nom !
            elif self.resource.format_type.extension.lower() in ('zip', 'tar'):
                outputformat = 'geojson'  # Il faudrait être sûr que le format existe avec le même nom !

            api[outputformat] = (
                '{base_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature'
                '&TYPENAME={typename}&OUTPUTFORMAT={outputformat}&CRSNAME=EPSG:{srid}'
            ).format(base_url=base_url,
                     typename=id,
                     outputformat=outputformat,
                     srid=str(DEFAULT_SRID))

        CkanHandler.update_resource(str(self.resource.ckan_id),
                                    api=json.dumps(api))
        CkanHandler.push_resource_view(title=name,
                                       description=description,
                                       resource_id=str(self.resource.ckan_id),
                                       view_type='geo_view')
Exemplo n.º 2
0
    def handle(self, *args, **options):
        total = Resource.objects.all().count()
        count = 0
        for resource in Resource.objects.all().order_by('id'):
            count += 1
            logger.info("[%d/%d] - Check Resource '%d'." %
                        (count, total, resource.pk))

            if not resource.up_file:
                continue
            # else:
            url = urljoin(resource.ckan_url,
                          'download/%s' % resource.up_file.name)

            CkanHandler.update_resource(str(resource.ckan_id), url=url)
            try:
                CkanHandler.update_resource(str(resource.ckan_id), url=url)
            except Exception as e:
                logger.error("Error with Resource '%d' - '%s'." %
                             (resource.pk, resource.title))
                logger.exception(e)
                logger.warning("Continue...")

            logger.info("Updated Resource '%d' - '%s' (filename: '%s')." %
                        (resource.pk, resource.title, resource.up_file.name))
Exemplo n.º 3
0
    def save(self,
             *args,
             current_user=None,
             synchronize=False,
             file_extras=None,
             **kwargs):

        # Version précédante de la ressource (avant modification)
        previous, created = self.pk \
            and (Resource.objects.get(pk=self.pk), False) or (None, True)

        if previous:
            # crs est immuable sauf si le jeu de données change (Cf. plus bas)
            self.crs = previous.crs

        # Quelques valeur par défaut à la création de l'instance
        if created or not (
                # Ou si l'éditeur n'est pas partenaire du CRIGE
                current_user and current_user.profile.crige_membership):

            # Mais seulement s'il s'agit de données SIG, sauf
            # qu'on ne le sait pas encore...
            self.geo_restriction = False
            self.ogc_services = True
            self.extractable = True

        # La restriction au territoire de compétence désactive toujours les services OGC
        if self.geo_restriction:
            self.ogc_services = False

        # Quelques contrôles sur les fichiers de données téléversée ou à télécharger
        filename = False
        content_type = None
        file_must_be_deleted = False  # permet d'indiquer si les fichiers doivent être supprimés à la fin de la chaine de traitement
        publish_raw_resource = True  # permet d'indiquer si les ressources brutes sont publiées dans CKAN

        if self.ftp_file:
            filename = self.ftp_file.file.name
            # Si la taille de fichier dépasse la limite autorisée,
            # on traite les données en fonction du type détecté
            if self.ftp_file.size > DOWNLOAD_SIZE_LIMIT:
                extension = self.format_type.extension.lower()
                if self.format_type.is_gis_format:
                    try:
                        gdalogr_obj = get_gdalogr_object(filename, extension)
                    except NotDataGISError:
                        # On essaye de traiter le jeux de données normalement, même si ça peut être long.
                        pass
                    else:
                        if gdalogr_obj.__class__.__name__ == 'GdalOpener':
                            s0 = str(self.ckan_id)
                            s1, s2, s3 = s0[:3], s0[3:6], s0[6:]
                            dir = os.path.join(CKAN_STORAGE_PATH, s1, s2)
                            os.makedirs(dir, mode=0o777, exist_ok=True)
                            shutil.copyfile(filename, os.path.join(dir, s3))

                            src = os.path.join(dir, s3)
                            dst = os.path.join(dir, filename.split('/')[-1])
                            try:
                                os.symlink(src, dst)
                            except FileNotFoundError as e:
                                logger.error(e)
                            else:
                                logger.debug(
                                    'Created a symbolic link {dst} pointing to {src}.'
                                    .format(dst=dst, src=src))

                        # if gdalogr_obj.__class__.__name__ == 'OgrOpener':
                        # On ne publie que le service OGC dans CKAN
                        publish_raw_resource = False

        elif (self.up_file and file_extras):
            # GDAL/OGR ne semble pas prendre de fichier en mémoire..
            # ..à vérifier mais si c'est possible comment indiquer le vsi en préfixe du filename ?
            super().save(*args, **kwargs)
            kwargs['force_insert'] = False

            filename = self.up_file.file.name
            file_must_be_deleted = True

        elif self.dl_url:
            try:
                directory, filename, content_type = download(
                    self.dl_url,
                    settings.MEDIA_ROOT,
                    max_size=DOWNLOAD_SIZE_LIMIT)
            except SizeLimitExceededError as e:
                l = len(str(e.max_size))
                if l > 6:
                    m = '{0} mo'.format(Decimal(int(e.max_size) / 1024 / 1024))
                elif l > 3:
                    m = '{0} ko'.format(Decimal(int(e.max_size) / 1024))
                else:
                    m = '{0} octets'.format(int(e.max_size))
                raise ValidationError(('La taille du fichier dépasse '
                                       'la limite autorisée : {0}.').format(m),
                                      code='dl_url')
            except Exception as e:
                if e.__class__.__name__ == 'HTTPError':
                    if e.response.status_code == 404:
                        msg = ('La ressource distante ne semble pas exister. '
                               "Assurez-vous que l'URL soit correcte.")
                    if e.response.status_code == 403:
                        msg = ("Vous n'avez pas l'autorisation pour "
                               'accéder à la ressource.')
                    if e.response.status_code == 401:
                        msg = ('Une authentification est nécessaire '
                               'pour accéder à la ressource.')
                else:
                    msg = 'Le téléchargement du fichier a échoué.'
                raise ValidationError(msg, code='dl_url')
            file_must_be_deleted = True

        # Synchronisation avec CKAN
        # =========================

        # La synchronisation doit s'effectuer avant la publication des
        # éventuelles couches de données SIG car dans le cas des données
        # de type « raster », nous utilisons le filestore de CKAN.
        if synchronize and publish_raw_resource:
            self.synchronize(content_type=content_type,
                             file_extras=file_extras,
                             filename=filename,
                             with_user=current_user)
        elif synchronize and not publish_raw_resource:
            url = reduce(urljoin, [
                settings.CKAN_URL, 'dataset/',
                str(self.dataset.ckan_id) + '/', 'resource/',
                str(self.ckan_id) + '/', 'download/',
                Path(self.ftp_file.name).name
            ])
            self.synchronize(url=url, with_user=current_user)

        # Détection des données SIG
        # =========================

        if filename:
            # On vérifie s'il s'agit de données SIG, uniquement pour
            # les extensions de fichier autorisées..
            extension = self.format_type.extension.lower()
            if self.format_type.is_gis_format:
                # Si c'est le cas, on monte les données dans la base PostGIS dédiée
                # et on déclare la couche au service OGC:WxS de l'organisation.

                # Mais d'abord, on vérifie si la ressource contient
                # déjà des « Layers », auquel cas il faudra vérifier si
                # la table de données a changée.
                existing_layers = {}
                if not created:
                    existing_layers = dict(
                        (re.sub('^(\w+)_[a-z0-9]{7}$', '\g<1>', layer.name),
                         layer.name) for layer in self.get_layers())

                try:

                    # C'est carrément moche mais c'est pour aller vite.
                    # Il faudrait factoriser tout ce bazar et créer
                    # un décorateur pour gérer le rool-back sur CKAN.

                    try:
                        gdalogr_obj = get_gdalogr_object(filename, extension)
                    except NotDataGISError:
                        tables = []
                        pass
                    else:

                        try:
                            self.format_type = ResourceFormats.objects.get(
                                extension=extension,
                                ckan_format=gdalogr_obj.format)
                        # except ResourceFormats.MultipleObjectsReturned:
                        #     pass
                        except Exception:
                            pass

                        # ==========================
                        # Jeu de données vectorielle
                        # ==========================

                        if gdalogr_obj.__class__.__name__ == 'OgrOpener':

                            # On convertit les données vers PostGIS

                            try:
                                tables = ogr2postgis(
                                    gdalogr_obj,
                                    update=existing_layers,
                                    epsg=self.crs and self.crs.auth_code
                                    or None,
                                    encoding=self.encoding)

                            except NotOGRError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    "Le fichier reçu n'est pas reconnu "
                                    'comme étant un jeu de données SIG correct.'
                                )
                                raise ValidationError(msg, code='__all__')

                            except DataDecodingError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Impossible de décoder correctement les '
                                    "données. Merci d'indiquer l'encodage "
                                    'ci-dessous.')
                                raise ValidationError(msg, code='encoding')

                            except WrongDataError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource contient des données SIG que '
                                    'nous ne parvenons pas à lire correctement. '
                                    'Un ou plusieurs objets sont erronés.')
                                raise ValidationError(msg)

                            except NotFoundSrsError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais nous ne parvenons pas à détecter le système '
                                    'de coordonnées. Merci de sélectionner le code du '
                                    'CRS dans la liste ci-dessous.')
                                raise ValidationError(msg, code='crs')

                            except NotSupportedSrsError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais le système de coordonnées de celles-ci '
                                    "n'est pas supporté par l'application.")
                                raise ValidationError(msg, code='__all__')

                            except ExceedsMaximumLayerNumberFixedError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                raise ValidationError(e.__str__(),
                                                      code='__all__')

                            else:
                                # Avant de créer des relations, l'objet doit exister
                                if created:
                                    # S'il s'agit d'une création, alors on sauve l'objet.
                                    super().save(*args, **kwargs)
                                    kwargs['force_insert'] = False

                                # Ensuite, pour tous les jeux de données SIG trouvés,
                                # on crée le service ows à travers la création de `Layer`
                                try:
                                    Layer = apps.get_model(
                                        app_label='idgo_admin',
                                        model_name='Layer')
                                    for table in tables:
                                        try:
                                            Layer.objects.get(name=table['id'],
                                                              resource=self)
                                        except Layer.DoesNotExist:
                                            save_opts = {
                                                'synchronize': synchronize
                                            }
                                            Layer.vector.create(
                                                bbox=table['bbox'],
                                                name=table['id'],
                                                resource=self,
                                                save_opts=save_opts)
                                except Exception as e:
                                    logger.error(e)
                                    file_must_be_deleted and remove_file(
                                        filename)
                                    for table in tables:
                                        drop_table(table['id'])
                                    raise e

                        # ==========================
                        # Jeu de données matricielle
                        # ==========================

                        if gdalogr_obj.__class__.__name__ == 'GdalOpener':

                            coverage = gdalogr_obj.get_coverage()

                            try:
                                tables = [
                                    gdalinfo(
                                        coverage,
                                        update=existing_layers,
                                        epsg=self.crs and self.crs.auth_code
                                        or None)
                                ]

                            except NotFoundSrsError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais nous ne parvenons pas à détecter le système '
                                    'de coordonnées. Merci de sélectionner le code du '
                                    'CRS dans la liste ci-dessous.')
                                raise ValidationError(msg, code='crs')

                            except NotSupportedSrsError as e:
                                logger.warning(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais le système de coordonnées de celles-ci '
                                    "n'est pas supporté par l'application.")
                                raise ValidationError(msg, code='__all__')

                            else:
                                if created:
                                    # S'il s'agit d'une création, alors on sauve l'objet.
                                    super().save(*args, **kwargs)
                                    kwargs['force_insert'] = False

                            # Super Crado Code
                            s0 = str(self.ckan_id)
                            s1, s2, s3 = s0[:3], s0[3:6], s0[6:]
                            dir = os.path.join(CKAN_STORAGE_PATH, s1, s2)
                            src = os.path.join(dir, s3)
                            dst = os.path.join(dir, filename.split('/')[-1])
                            try:
                                os.symlink(src, dst)
                            except FileExistsError as e:
                                logger.warning(e)
                            except FileNotFoundError as e:
                                logger.error(e)
                            else:
                                logger.debug(
                                    'Created a symbolic link {dst} pointing to {src}.'
                                    .format(dst=dst, src=src))

                            try:
                                Layer = apps.get_model(app_label='idgo_admin',
                                                       model_name='Layer')
                                for table in tables:
                                    try:
                                        Layer.objects.get(name=table['id'],
                                                          resource=self)
                                    except Layer.DoesNotExist:
                                        Layer.raster.create(bbox=table['bbox'],
                                                            name=table['id'],
                                                            resource=self)
                            except Exception as e:
                                logger.error(e)
                                file_must_be_deleted and remove_file(filename)
                                raise e

                except Exception as e:
                    if created:
                        if current_user:
                            username = current_user.username
                            apikey = CkanHandler.get_user(username)['apikey']
                            with CkanUserHandler(apikey) as ckan:
                                ckan.delete_resource(str(self.ckan_id))
                        else:
                            CkanHandler.delete_resource(str(self.ckan_id))
                        for layer in self.get_layers():
                            layer.delete(current_user=current_user)
                    # Puis on « raise » l'erreur
                    raise e

                # On met à jour les champs de la ressource
                SupportedCrs = apps.get_model(app_label='idgo_admin',
                                              model_name='SupportedCrs')
                crs = [
                    SupportedCrs.objects.get(auth_name='EPSG',
                                             auth_code=table['epsg'])
                    for table in tables
                ]
                # On prend la première valeur (c'est moche)
                self.crs = crs and crs[0] or None

                # Si les données changent..
                if existing_layers and \
                        previous.get_layers() != self.get_layers():
                    # on supprime les anciens `layers`..
                    for layer in previous.get_layers():
                        layer.delete()
        ####
        if self.get_layers():
            extent = self.get_layers().aggregate(
                models.Extent('bbox')).get('bbox__extent')
            if extent:
                xmin, ymin = extent[0], extent[1]
                xmax, ymax = extent[2], extent[3]
                setattr(self, 'bbox', bounds_to_wkt(xmin, ymin, xmax, ymax))
        else:
            # Si la ressource n'est pas de type SIG, on passe les trois arguments
            # qui concernent exclusivement ces dernières à « False ».
            self.geo_restriction = False
            self.ogc_services = False
            self.extractable = False

        super().save(*args, **kwargs)

        # Puis dans tous les cas..
        # on met à jour le statut des couches du service cartographique..
        if not created:
            self.update_enable_layers_status()

        # on supprime les données téléversées ou téléchargées..
        if file_must_be_deleted:
            remove_file(filename)

        # [Crado] on met à jour la ressource CKAN
        if synchronize:
            CkanHandler.update_resource(str(self.ckan_id),
                                        extracting_service=str(
                                            self.extractable))

        for layer in self.get_layers():
            layer.save(synchronize=synchronize)

        self.dataset.date_modification = timezone.now().date()
        self.dataset.save(current_user=None,
                          synchronize=True,
                          update_fields=['date_modification'])
Exemplo n.º 4
0
    def save(self,
             *args,
             current_user=None,
             synchronize=True,
             activate=None,
             **kwargs):

        # Version précédante du jeu de données (avant modification)
        previous, created = self.pk \
            and (Dataset.objects.get(pk=self.pk), False) or (None, True)
        if created:
            activate = True

        # Quelques valeurs par défaut
        # ===========================
        today = timezone.now().date()
        if not self.date_creation:  # La date de création
            self.date_creation = today
        if not self.date_modification:  # La date de modification
            self.date_modification = today
        if not self.date_publication:  # La date de publication
            self.date_publication = today

        if not self.owner_name:  # Le propriétaire du jeu de données
            self.owner_name = self.editor.get_full_name()
        if not self.owner_email:  # et son e-mail
            self.owner_email = self.editor.email

        # Le rectangle englobant du jeu de données :
        #     Il est calculé en fonction des ressources géographiques et/ou de la couverture
        #     et/ou de la couverture géographique définie
        layers = self.get_layers()
        if layers:
            # On calcule la BBOX de l'ensemble des Layers rattachés au Dataset
            extent = layers.aggregate(
                models.Extent('bbox')).get('bbox__extent')
            if extent:
                xmin, ymin = extent[0], extent[1]
                xmax, ymax = extent[2], extent[3]
                setattr(self, 'bbox', bounds_to_wkt(xmin, ymin, xmax, ymax))
        else:
            # Sinon, on regarde la valeur de `geocover` renseignée
            if self.geocover == 'jurisdiction':
                # Prend l'étendue du territoire de compétence
                if self.organisation:
                    jurisdiction = self.organisation.jurisdiction
                    if jurisdiction and jurisdiction.communes:
                        bounds = jurisdiction.get_bounds()
                        if bounds:
                            xmin, ymin = bounds[0][1], bounds[0][0]
                            xmax, ymax = bounds[1][1], bounds[1][0]
                            setattr(self, 'bbox',
                                    bounds_to_wkt(xmin, ymin, xmax, ymax))
            elif self.geocover == 'regionale':
                # Prend l'étendue par défaut définie en settings
                setattr(self, 'bbox', DEFAULT_BBOX)
            else:
                setattr(self, 'bbox', self.bbox
                        or None)  # ATTENTION AUX EFFETS DE BORD !

        # On sauvegarde le jeu de données
        super().save(*args, **kwargs)

        # Puis...
        if not created:
            # Une organisation CKAN ne contenant plus
            # de jeu de données doit être désactivée.
            if previous.organisation:
                CkanHandler.deactivate_ckan_organisation_if_empty(
                    str(previous.organisation.ckan_id))

            # On vérifie si l'organisation du jeu de données change.
            # Si c'est le cas, il est nécessaire de sauvegarder tous
            # les `Layers` rattachés au jeu de données afin de forcer
            # la modification du `Workspace` (c'est-à-dire du Mapfile)
            if previous.organisation != self.organisation:
                for resource in previous.get_resources():
                    for layer in resource.get_layers():
                        layer.save()
                        url = '{0}#{1}'.format(
                            OWS_URL_PATTERN.format(
                                organisation=self.organisation.slug),
                            layer.name)
                        CkanHandler.update_resource(layer.name, url=url)

        # Enfin...
        if synchronize:
            ckan_dataset = self.synchronize(with_user=current_user,
                                            activate=activate)
            # puis on met à jour `ckan_id`
            self.ckan_id = UUID(ckan_dataset['id'])
            super().save(update_fields=['ckan_id'])