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')
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))
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'])
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'])