Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
438 changes: 406 additions & 32 deletions src/routes/qgis.py

Large diffs are not rendered by default.

66 changes: 31 additions & 35 deletions src/routes/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import xml.etree.ElementTree as ET
from datetime import datetime
from os import makedirs, mkdir, path, walk
from shutil import rmtree, copyfileobj, move
from shutil import rmtree, copyfileobj
from typing import cast
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
from zipfile import BadZipFile, ZipFile
Expand Down Expand Up @@ -135,15 +135,10 @@ def _qgs_project_payload(qgs_dir: str, absolute_file: str) -> dict:

def _extract_qgs_zip(
uploaded_file, qgs_dir: str, filename: str
) -> tuple[str, list[dict]]:
) -> tuple[str, list[dict], bool]:
archive_name = path.splitext(filename)[0]
destination_dir = path.join(qgs_dir, archive_name)

if path.exists(destination_dir):
rmtree(destination_dir, ignore_errors=True)

mkdir(destination_dir)

overwritten = False
try:
with ZipFile(uploaded_file.stream) as archive:
file_members = [
Expand All @@ -161,6 +156,8 @@ def _extract_qgs_zip(
if len(top_level_parts) == 1 and len(file_members) > 0:
root_prefix = f"{top_level_parts.pop()}/"

normalized_members = []
qgs_members = []
for member in file_members:
normalized_name = member.filename
if root_prefix and normalized_name.startswith(root_prefix):
Expand All @@ -170,6 +167,25 @@ def _extract_qgs_zip(
if not normalized_name:
continue

normalized_members.append((member, normalized_name))
if normalized_name.lower().endswith(".qgs"):
qgs_members.append(normalized_name)

if not qgs_members:
raise BadRequest("ZIP archive does not contain any .qgs file")

if len(qgs_members) == 1:
project_name = path.splitext(path.basename(qgs_members[0]))[0]
destination_dir = path.join(qgs_dir, project_name)
else:
destination_dir = path.join(qgs_dir, archive_name)

overwritten = path.exists(destination_dir)
if overwritten:
rmtree(destination_dir, ignore_errors=True)
mkdir(destination_dir)

for member, normalized_name in normalized_members:
destination = path.abspath(path.join(destination_dir, normalized_name))
if path.commonpath(
[path.abspath(destination_dir), destination]
Expand All @@ -183,7 +199,8 @@ def _extract_qgs_zip(
with archive.open(member) as source, open(destination, "wb") as target:
copyfileobj(source, target)
except BadZipFile:
rmtree(destination_dir, ignore_errors=True)
if path.exists(destination_dir):
rmtree(destination_dir, ignore_errors=True)
raise BadRequest("Invalid ZIP archive")
except Exception:
if path.exists(destination_dir):
Expand All @@ -196,40 +213,19 @@ def _extract_qgs_zip(
if extracted_file.lower().endswith(".qgs"):
extracted_projects.append(path.join(root, extracted_file))

if not extracted_projects:
rmtree(destination_dir, ignore_errors=True)
raise BadRequest("ZIP archive does not contain any .qgs file")

if len(extracted_projects) == 1:
project_file = extracted_projects[0]
project_name = path.splitext(path.basename(project_file))[0]
final_destination_dir = path.join(qgs_dir, project_name)

if path.abspath(final_destination_dir) != path.abspath(destination_dir):
if path.exists(final_destination_dir):
rmtree(final_destination_dir, ignore_errors=True)
move(destination_dir, final_destination_dir)
destination_dir = final_destination_dir

extracted_projects = [
path.join(root, extracted_file)
for root, _, files in walk(destination_dir)
for extracted_file in files
if extracted_file.lower().endswith(".qgs")
]

extracted_payloads = [
_qgs_project_payload(qgs_dir, absolute_file) for absolute_file in extracted_projects
]
extracted_payloads.sort(key=lambda item: item["path"].lower())
return destination_dir, extracted_payloads
return destination_dir, extracted_payloads, overwritten


def _store_qgs_file(uploaded_file, qgs_dir: str, filename: str) -> dict:
def _store_qgs_file(uploaded_file, qgs_dir: str, filename: str) -> tuple[dict, bool]:
project_name = path.splitext(filename)[0]
destination_dir = path.join(qgs_dir, project_name)
overwritten = path.exists(destination_dir)

if path.exists(destination_dir):
if overwritten:
rmtree(destination_dir, ignore_errors=True)

mkdir(destination_dir)
Expand All @@ -241,7 +237,7 @@ def _store_qgs_file(uploaded_file, qgs_dir: str, filename: str) -> dict:
rmtree(destination_dir, ignore_errors=True)
raise

return _qgs_project_payload(qgs_dir, destination)
return _qgs_project_payload(qgs_dir, destination), overwritten


@basic_store.record_once
Expand Down
35 changes: 35 additions & 0 deletions src/static/css/mviewerstudio.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@
vertical-align: middle;
margin-left: 3px;
}

.qgis-project-actions {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}

.qgis-project-action-btn {
width: 2.2rem;
height: 2.2rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.45rem;
}

.qgis-project-action-btn i {
font-size: 1rem;
line-height: 1;
}

.qgis-project-action-btn-copy {
color: #7c3aed;
border: 1px solid #7c3aed;
background: #ffffff;
}

.qgis-project-action-btn-copy:hover,
.qgis-project-action-btn-copy:focus {
color: #ffffff;
background: #7c3aed;
border-color: #7c3aed;
}


/* Alert */
Expand Down
34 changes: 29 additions & 5 deletions src/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,31 @@ <h5 class="modal-title" i18n="modal.file.import">Ouvrir un projet existant</h5>
<button id="createConfigFromQgisAppUrl" type="button" class="btn btn-info"><i class="ri-search-2-line"></i></button>
</div>
</div>

<div class="mt-4" id="qgis-projects-list-container">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="mb-0">Configurations QGIS Server disponibles :</label>
<button type="button" class="btn btn-outline-info btn-sm" id="refresh-qgis-projects-list">
<i class="bi bi-arrow-clockwise"></i>
<span>Actualiser</span>
</button>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Projet</th>
<th>Action</th>
</tr>
</thead>
<tbody id="qgis-projects-table-body">
<tr>
<td colspan="2" class="text-muted">Chargement…</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -1794,7 +1819,7 @@ <h5 class="modal-title" i18n="modal.layer.new.searchdata">Ajouter une donnée</h
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#newlayer-qgs" type="button" role="tab" aria-controls="newlayer-qgs" aria-selected="false" onclick="window.qgis && qgis.showQgsProjectInputs();">
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#newlayer-qgs" type="button" role="tab" aria-controls="newlayer-qgs" aria-selected="false">
<span>Depuis un projet qgis</span>
</a>
</li>
Expand Down Expand Up @@ -1906,7 +1931,7 @@ <h6>Paramètres du projet QGIS</h6>
<div class="input-group">
<input type="text" class="form-control" id="newlayer-qgs-url" placeholder="Lien vers le projet QGIS Server">
<div class="input-group-append">
<button class="btn btn-info" id="qgs_search_layers_btn" type="button" onclick="qgis.searchProjectLayers();" disabled><i class="bi bi-search"></i></button>
<button class="btn btn-info" id="qgs_search_layers_btn" type="button" disabled><i class="bi bi-search"></i></button>
</div>
</div>
</div>
Expand All @@ -1916,7 +1941,7 @@ <h6>Paramètres du projet QGIS</h6>
<div class="col-md-12">
<div class="form-group">
<label class="control-label" for="qgs-project-list">Couche du projet QGIS</label>
<select id="qgs-project-list" class="form-control" onchange="selectQgsLayer(this)">
<select id="qgs-project-list" class="form-control">
<option value="" selected>Choisir une couche</option>
</select>
</div>
Expand Down Expand Up @@ -2090,9 +2115,8 @@ <h5 class="modal-title" i18n="template.generator">Générateur de template</h5>
<script src="js/wizard.js"></script>
<script src="js/manageDisplayContainer.js"></script>
<script src="lib/icon-picker/iconPickerComponent.js"></script>
<script src="js/utils/qgis/qgisToMviewer.js"></script>
<script src="js/utils/qgis/qgis.js"></script>
<script src="js/mviewerstudio.js"></script>
<script type="module" src="js/utils/qgis/qgis.js"></script>
<script type="module" src="lib/icon-picker/icon-picker.js"></script>
<script src="lib/bootstrap-table/bootstrap-table.min.js"></script>
<script src="lib/bootstrap-table/bootstrap-table-fr-FR.min.js"></script>
Expand Down
59 changes: 43 additions & 16 deletions src/static/js/mviewerstudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,40 @@ var API = {};

var mviewer = {};

$(document).ready(function () {
//Get URL Parameters
if (window.location.search) {
$.extend(
API,
$.parseJSON(
'{"' +
decodeURIComponent(
window.location.search.substring(1).replace(/&/g, '","').replace(/=/g, '":"')
) +
'"}'
)
);
/**
* Reads URL parameters from both the query string and the ``#?`` hash syntax.
*
* This keeps backward compatibility with the existing query-string behavior
* while allowing external applications to open Studio with
* `index.html#?config=<xml-url>`.
*
* @returns {Object.<string, string>} Flat map of URL parameters.
*/
var readUrlParameters = function () {
const params = new URLSearchParams(window.location.search);
const hash = window.location.hash || "";
const hashQuery = hash.startsWith("#?")
? hash.slice(2)
: hash.startsWith("#")
? hash.slice(1)
: "";

if (hashQuery) {
new URLSearchParams(hashQuery).forEach((value, key) => {
params.set(key, value);
});
}

const parsedParams = {};
params.forEach((value, key) => {
parsedParams[key] = value;
});

return parsedParams;
};

$(document).ready(function () {
API = readUrlParameters();
fetch("config.json", {
method: "GET",
header: {
Expand Down Expand Up @@ -133,14 +153,18 @@ $(document).ready(function () {
}
}

if (API.xml) {
if (API.config) {
loadApplicationParametersFromRemoteFile(API.config);
} else if (API.xml) {
loadApplicationParametersFromRemoteFile(API.xml);
} else if (API.wmc) {
loadApplicationParametersFromWMC(API.wmc);
} else {
newConfiguration();
}

document.dispatchEvent(new CustomEvent("mviewerstudio:qgis:init-stored-projects"));

updateProviderSearchButtonState();

// Default params for layers
Expand Down Expand Up @@ -1336,8 +1360,11 @@ var loadApplicationParametersFromFile = function () {
return;
}

if (window.qgis && typeof qgis.loadApplicationParametersFromFileInput === "function") {
qgis.loadApplicationParametersFromFileInput();
const qgisImportEvent = new CustomEvent(
"mviewerstudio:qgis:load-application-parameters-from-file-input",
{ cancelable: true }
);
if (!document.dispatchEvent(qgisImportEvent)) {
return;
}

Expand Down
Loading
Loading