@@ -1961,6 +1965,49 @@ var mv = (function () {
'data-url':row.xml
}
},
+ nameNormalizer: (str="") => {
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^\w ]/g, '_').replace(/\s/g, '_').toLowerCase()
+ },
+ onChangeName: ({value}, defaultValue) => {
+ document.querySelector("#sendPublishApp").disabled = conflict && v === defaultName;
+ },
+ showNamePublishModal: (id, name = "", conflict = false) => {
+ if (config.relation) {
+ return mv.publish(config.id, config.relation)
+ }
+ const defaultName = name ? mv.nameNormalizer(name) : mv.nameNormalizer(document.querySelector("#opt-title").value);
+ const publishAppModal = new bootstrap.Modal('#genericModal');
+ const question = conflict ? "Ce nom existe déjà !
Vous pouvez changer le nom ou annuler." : "Quel nom souhaitez vous utiliser pour la publication ?"
+ genericModalContent.innerHTML = "";
+ genericModalContent.innerHTML = `
+
+
+ `;
+ publishAppModal.show();
+ },
showPublishModal: (shareLink = "", iframeLink = "", draftLink = "") => {
const publishModal = new bootstrap.Modal('#genericModal');
genericModalContent.innerHTML = "";
@@ -1999,16 +2046,16 @@ var mv = (function () {
`;
publishModal.show();
},
- publish: (id) => {
+ publish: (id, name = "") => {
if (!id) {
return alertCustom("L'ID n'est pas renseigné. Veuillez contacter un administrateur.", "danger");
}
if (!config.isFile) {
return alertCustom("Enregistrez une premère fois avant de publier !", "danger");
}
- fetch(`${ _conf.api }/${id}/publish`)
+ fetch(`${ _conf.api }/${id}/publish/${name}`,)
.then(r => {
- return r.ok ? r.json() : Promise.reject(r)
+ return r.ok ? r.json() : Promise.reject(r);
})
.then(data => {
if (!_conf?.mviewer_publish) {
@@ -2026,7 +2073,11 @@ var mv = (function () {
alertCustom("L'application a bien été publiée !", "success");
})
.catch(err => {
- alertCustom("Une erreur s'est produite. Veuillez contacter un administrateur.", "danger");
+ if (err.status == 409) {
+ mv.showNamePublishModal(id, name, true);
+ } else {
+ alertCustom("Une erreur s'est produite. Veuillez contacter un administrateur.", "danger");
+ }
})
},
refreshOnPublish: (file) => {
@@ -2034,7 +2085,7 @@ var mv = (function () {
loadApplicationParametersFromRemoteFile(url);
},
unpublish: (id) => {
- fetch(`${ _conf.api }/${ id }/publish`, { method: "DELETE" })
+ fetch(`${ _conf.api }/${ id }/publish/${config.relation}`, { method: "DELETE" })
.then(r => {
return r.ok ? r.json() : Promise.reject(r)
})
diff --git a/mviewerstudio.i18n.json b/mviewerstudio.i18n.json
index 45610814..f306b41e 100644
--- a/mviewerstudio.i18n.json
+++ b/mviewerstudio.i18n.json
@@ -147,6 +147,8 @@
"tabs.publication.preview_title" : "Prévisualiser votre application",
"tabs.publication.preview_text" : "Tester et valider votre configuration en live",
"tabs.publication.publish_title" : "Publier votre application",
+ "tabs.publication.publish_retry" : "Publier avec ce nouveau nom",
+ "tabs.publication.publish_replace": "Remplacer",
"tabs.publication.publish_text" : "Mettre en ligne votre application et obtenez un lien de partage",
"tabs.data.title": "Thématiques & données",
"tabs.data.themespanel.title": "Panneau des thématiques",
@@ -530,6 +532,8 @@
"version.comment.ph": "ex: Test release without data",
"studio.toolsbar.unpublish": "Unpublish",
"tabs.publication.publish_title" : "Publish application",
- "tabs.publication.publish_text" : "Put your application online."
+ "tabs.publication.publish_text" : "Put your application online.",
+ "tabs.publication.publish_retry": "Retry with this new name",
+ "tabs.publication.publish_replace": "Replace"
}
}
diff --git a/srv/python/mviewerstudio_backend/models/config.py b/srv/python/mviewerstudio_backend/models/config.py
index 96e2dd08..8e533722 100644
--- a/srv/python/mviewerstudio_backend/models/config.py
+++ b/srv/python/mviewerstudio_backend/models/config.py
@@ -21,7 +21,7 @@ class ConfigModel:
publisher: str
subject: str
date: str
- publish: bool
+ relation: str
def as_dict(self):
return {
@@ -35,5 +35,5 @@ def as_dict(self):
"url": self.url,
"subject": self.subject,
"date": self.date,
- "publish": self.publish
+ "relation": self.relation
}
diff --git a/srv/python/mviewerstudio_backend/route.py b/srv/python/mviewerstudio_backend/route.py
index 1ba77928..65fd1982 100644
--- a/srv/python/mviewerstudio_backend/route.py
+++ b/srv/python/mviewerstudio_backend/route.py
@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify, Response, request, current_app, redirect
from .utils.login_utils import current_user
-from .utils.config_utils import Config
+from .utils.config_utils import Config, edit_xml_string, read_xml_file_content
from .utils.commons import clean_preview, init_preview
import hashlib, uuid
from os import path, mkdir, remove
@@ -11,7 +11,7 @@
from .utils.git_utils import Git_manager
from .utils.register_utils import from_xml_path
-from werkzeug.exceptions import BadRequest, MethodNotAllowed
+from werkzeug.exceptions import BadRequest, MethodNotAllowed, Conflict
import logging
@@ -126,8 +126,8 @@ def list_stored_mviewer_config() -> Response:
config["url"] = current_app.config["CONF_PATH_FROM_MVIEWER"] + config["url"]
return jsonify(configs)
-@basic_store.route("/api/app/
/publish", methods=["GET", "DELETE"])
-def publish_mviewer_config(id) -> Response:
+@basic_store.route("/api/app//publish/", methods=["GET", "DELETE"])
+def publish_mviewer_config(id, name) -> Response:
"""
Will put online a config.
This route will copy / past XML to publication directory or delete to unpublish.
@@ -135,35 +135,58 @@ def publish_mviewer_config(id) -> Response:
"""
logger.debug("PUBLISH : %s " % id)
+ xml_publish_name = name
+
+ # control publish directory exists
publish_dir = current_app.config["MVIEWERSTUDIO_PUBLISH_PATH"]
if not publish_dir or not path.exists(publish_dir):
return BadRequest("Publish directory does not exists !")
+ # create or get org parent directory from publication path
+ org_publish_dir = path.join(current_app.publish_path, current_user.organisation)
+ if not path.exists(org_publish_dir):
+ mkdir(org_publish_dir)
+
+ # control file to create or replace
+ past_file = path.join(org_publish_dir, "%s.xml" % xml_publish_name)
+ if path.exists(past_file) and request.method == "GET":
+ # read file to replace
+ content = read_xml_file_content(past_file)
+ org = content.find(".//metadata/{*}RDF/{*}Description//{*}publisher").text
+ creator = content.find(".//metadata/{*}RDF/{*}Description//{*}creator").text
+ identifier = content.find(".//metadata/{*}RDF/{*}Description//{*}identifier").text
+ lastRelation = content.find(".//metadata/{*}RDF/{*}Description//{*}relation").text
+ # detect conflict
+ if lastRelation != xml_publish_name or org != current_user.organisation or creator != current_user.username or identifier != id:
+ return Conflict("Already exists !")
+ # replace safely or return bad request
+ remove(past_file)
+ # control that workspace to copy exists
workspace = path.join(current_app.config["EXPORT_CONF_FOLDER"], current_user.organisation, id)
-
if not path.exists(workspace):
return BadRequest("Application does not exists !")
-
- config = current_app.register.read_json(id)
+ # read config if exists
+ config = current_app.register.read_json(id)
if not config:
raise BadRequest("This config doesn't exists !")
copy_file = current_app.config["EXPORT_CONF_FOLDER"] + config[0]["url"]
config = from_xml_path(current_app, copy_file)
- past_file = path.join(current_app.publish_path, "%s.xml" % id)
-
# add publish info in XML
if request.method == "GET":
- config.xml.set("publish", "true")
+ edit_xml_string(config.meta, "relation", xml_publish_name)
message = "publish"
# add unpublish info in XML
if request.method == "DELETE":
- config.xml.set("publish", "false")
+ edit_xml_string(config.meta, "relation", "")
+ remove(past_file)
message = "Unpublish"
+ past_file = None
+ # will update XML with correct relation value to map publish and draft files
config.write()
# commit to track this action
@@ -172,15 +195,10 @@ def publish_mviewer_config(id) -> Response:
# update JSON
config.register.update_from_id(id)
- if path.exists(past_file):
- remove(past_file)
-
# move to publish directory
if request.method == "GET":
copyfile(copy_file, past_file)
- if request.method == "DELETE":
- past_file=None
draft_file = current_app.config["CONF_PATH_FROM_MVIEWER"] + config.as_dict()["url"]
return jsonify({"online_file": past_file, "draft_file": draft_file})
@@ -212,7 +230,13 @@ def delete_config_workspace(id = None) -> Response:
if current_user.username == "anonymous" and config[0]["publisher"] != current_app.config["DEFAULT_ORG"]:
logger.debug("DELETE : NOT ALLOWED FOR THIS ANONYMOUS USER - ORG IS NOT DEFAULT")
return MethodNotAllowed("Not allowed !")
-
+ # delete publish
+ if "relation" in config[0] and config[0]["relation"]:
+ org_publish_dir = path.join(current_app.publish_path, current_user.organisation)
+ publish_file = path.join(org_publish_dir, "%s.xml" % config[0]["relation"])
+ if path.exists(publish_file):
+ remove(publish_file)
+
# delete in json
current_app.register.delete(id)
# delete dir
diff --git a/srv/python/mviewerstudio_backend/utils/config_utils.py b/srv/python/mviewerstudio_backend/utils/config_utils.py
index 348bcf83..9ffe67d0 100644
--- a/srv/python/mviewerstudio_backend/utils/config_utils.py
+++ b/srv/python/mviewerstudio_backend/utils/config_utils.py
@@ -1,4 +1,4 @@
-from os import path, makedirs
+from os import path, makedirs, remove
from unidecode import unidecode
import logging
import xml.etree.ElementTree as ET
@@ -12,23 +12,69 @@
logger = logging.getLogger(__name__)
-def getWorkspace(app, path_id = ""):
- return path.join(app.config["EXPORT_CONF_FOLDER"], path_id)
+def getWorkspace(app, end_path = ""):
+ '''
+ Retrieve workspace from path
+ :param app: current Flask app instance
+ :param path_id: path
+ '''
+ if not end_path : return
+ return path.join(app.config["EXPORT_CONF_FOLDER"], end_path)
def edit_xml_string(root, attribute, value):
+ '''
+ Tool to find and write inside XML node
+ :param root: xml or xml root node object (e.g self.meta in Config class)
+ :param attribute: string xml node name to change in root node (e.g "{*}relation")
+ :param value: string value to insert in XML node
+ '''
attr = root.find(".//{*}%s" % attribute)
attr.text = value
def write_file(xml, xml_path):
+ '''
+ Will write an xml into file
+ :param xml: xml to write in xml path file
+ :param xml_path: string xml file path
+ '''
xml_to_string = ET.tostring(xml).decode("utf-8")
file = open(xml_path, "w")
file.write(xml_to_string)
file.close()
+def clean_xml_from_dir(path):
+ '''
+ Remove each XML found in dir
+ :param path: string xml file path
+ '''
+ for file in glob.glob("%s/*.xml" % path):
+ remove(file)
+
+def read_xml_file_content(path, node = ""):
+ '''
+ Read XML from path and could return xml node or xml as parser
+ :param path: string xml file path
+ '''
+ fileToReplace = open(path, "r")
+ xml_str = fileToReplace.read()
+ xml_parser = ET.fromstring(xml_str)
+ if node :
+ return xml_parser.find(node)
+ else:
+ return xml_parser
+
+def control_relation(path, relation):
+ content = read_xml_file_content(path)
+ org = content.find(".//metadata/{*}RDF/{*}Description//{*}publisher").text
+ creator = content.find(".//metadata/{*}RDF/{*}Description//{*}creator").text
+ identifier = content.find(".//metadata/{*}RDF/{*}Description//{*}identifier").text
+ lastRelation = content.find(".//metadata/{*}RDF/{*}Description//{*}relation").text
+
+
'''
This class ease git repo manipulations.
A register from store/register.json is use as global configs metadata store.
-DCAT-RDF Metadata are given by front end (see above ConfigModel).
+DCAT-RDF Metadata are given by front end (see imported ConfigModel).
'''
class Config:
def __init__(self, data = "", app = None, xml = None) -> None:
@@ -108,16 +154,12 @@ def _get_xml_describe(self, xml):
edit_xml_string(meta_root, "creator", current_user.username)
if current_user and current_user.organisation:
edit_xml_string(meta_root, "publisher", current_user.organisation)
- if meta_root.find(".//{*}relation") is not None:
- decode_file_name = unidecode(meta_root.find(".//{*}relation").text)
- normalize_file_name = re.sub('[^a-zA-Z0-9 \n\.]', "_", decode_file_name).replace(" ", "_")
- edit_xml_string(meta_root, "relation", normalize_file_name)
return meta_root
def write(self):
write_file(self.xml, self.full_xml_path)
- def create_or_update_config(self, file):
+ def create_or_update_config(self, file = None):
'''
Create config workspace and save XML as file.
Will init git file as version manager.
@@ -131,9 +173,13 @@ def create_or_update_config(self, file):
# get meta info from XML
if self.meta.find(".//{*}identifier"):
self.uuid = self.meta.find(".//{*}identifier").text
- file_name = self.meta.find("{*}relation").text
+
+ # normalize file name
+ app_name = self.meta.find("{*}title").text[:20]
+ decode_file_name = unidecode(app_name)
+ normalized_file_name = re.sub('[^a-zA-Z0-9 \n\.]', "_", decode_file_name).replace(" ", "_")
# save file
- self.full_xml_path = path.join(self.workspace, "%s.xml" % file_name)
+ self.full_xml_path = path.join(self.workspace, "%s.xml" % normalized_file_name)
# write file
self.write()
@@ -159,11 +205,17 @@ def as_data(self):
publisher = self.meta.find("{*}publisher").text,
url = url,
subject = subject,
- publish = self.xml.get("publish")
+ relation = self.meta.find("{*}relation").text if self.meta.find("{*}relation").text else ""
)
def as_dict(self):
'''
Get config as dict.
'''
- return self.as_data().as_dict()
\ No newline at end of file
+ return self.as_data().as_dict()
+
+ def get(self, prop):
+ dict_data = self.as_data().as_dict()
+ if prop not in dict_data:
+ return
+ return dict_data[prop]
\ No newline at end of file
diff --git a/srv/python/mviewerstudio_backend/utils/register_utils.py b/srv/python/mviewerstudio_backend/utils/register_utils.py
index ab8cdda7..caa9f5fd 100644
--- a/srv/python/mviewerstudio_backend/utils/register_utils.py
+++ b/srv/python/mviewerstudio_backend/utils/register_utils.py
@@ -131,7 +131,7 @@ def update_from_id(self, id):
This allow to change XML manually and update register automatically on service startup.
:param id: string uuid use as unique directory name
'''
- xml_path = glob.glob("%s/*.xml" % path.join(self.store_directory, id))
+ xml_path = glob.glob("%s/*.xml" % path.join(self.store_directory, current_user.organisation, id))
if xml_path:
config = from_xml_path(self.app, xml_path[0])
config_dict = config.as_dict()