diff --git a/assets/src/components/FeatureToolbar.js b/assets/src/components/FeatureToolbar.js
index 5850b679f0..48876d20b4 100644
--- a/assets/src/components/FeatureToolbar.js
+++ b/assets/src/components/FeatureToolbar.js
@@ -15,6 +15,8 @@ export default class FeatureToolbar extends HTMLElement {
super();
[this._layerId, this._fid] = this.getAttribute('value').split('.');
+ [this._pivotLayerId, this._parentFeatureId] = (this.getAttribute('pivot-layer') && this.getAttribute('pivot-layer').split(':') ) || [null, null];
+ [this._pivotType, this._pivotLayerConfig] = this._pivotLayerId ? lizMap.getLayerConfigById(this._pivotLayerId) : [null, null];
[this._featureType, this._layerConfig] = lizMap.getLayerConfigById(this.layerId);
this._typeName = this._layerConfig?.shortname || this._layerConfig?.typename || this._layerConfig?.name;
this._parentLayerId = this.getAttribute('parent-layer-id');
@@ -36,8 +38,8 @@ export default class FeatureToolbar extends HTMLElement {
this.zoom()} title="${lizDict['attributeLayers.btn.zoom.title']}">
this.center()} title="${lizDict['attributeLayers.btn.center.title']}">
this.edit()} title="${lizDict['attributeLayers.btn.edit.title']}">
- this.delete()} title="${lizDict['attributeLayers.btn.delete.title']}">
- this.isLayerPivot ? this.delete() : this.unlink()} title="${lizDict['attributeLayers.btn.remove.link.title']}">
+ this.delete()} title="${lizDict['attributeLayers.btn.delete.title']}">
+ this.isNToMRelation ? this.deleteFromPivot() : this.unlink()} title="${lizDict['attributeLayers.btn.remove.link.title']}">
${this.isFeatureExportable
@@ -159,6 +161,23 @@ export default class FeatureToolbar extends HTMLElement {
return this._parentLayerId;
}
+ get pivotLayerId(){
+ const pivotAttributeLayerConf = lizMap.getLayerConfigById( this._pivotLayerId, lizMap.config.attributeLayers, 'layerId' );
+ const config = lizMap.config;
+ if (pivotAttributeLayerConf
+ && pivotAttributeLayerConf[1]?.pivot == 'True'
+ && config.relations.pivot
+ && config.relations.pivot[this._pivotLayerId]
+ && config.relations.pivot[this._pivotLayerId][this.layerId]
+ && config.relations.pivot[this._pivotLayerId][this.parentLayerId]
+ ){
+ return this._pivotLayerId;
+ }
+
+ return null;
+
+ }
+
get isSelected() {
const selectedFeatures = this._layerConfig?.['selectedFeatures'];
return selectedFeatures && selectedFeatures.includes(this.fid);
@@ -187,19 +206,70 @@ export default class FeatureToolbar extends HTMLElement {
return lizMap.getLayerConfigById(this.layerId, lizMap.config.attributeLayers, 'layerId')?.[1];
}
+ get pivotAttributeTableConfig(){
+ return lizMap.getLayerConfigById(this.pivotLayerId, lizMap.config.attributeLayers,'layerId')?.[1];
+ }
+
get isLayerEditable(){
return lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.modifyAttribute === "True"
|| lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.modifyGeometry === "True";
}
- get isLayerPivot(){
- return this.attributeTableConfig?.['pivot'] === 'True';
+ get isNToMRelation() {
+ if (this.pivotLayerId) return true;
+ else return false;
}
get isUnlinkable(){
return this.parentLayerId &&
- (this.isLayerEditable && !this.isLayerPivot) ||
- (lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.deleteFeature === "True" && this.isLayerPivot);
+ (this.isLayerEditable && !this.isNToMRelation) ||
+ (lizMap.config?.editionLayers?.[this._pivotType]?.capabilities?.deleteFeature === "True" && this.isNToMRelation);
+ }
+
+ get pivotFeatureId(){
+ const pivotLayerId = this.pivotLayerId;
+
+ if(!pivotLayerId) return null;
+
+ const parentLayerId = this.parentLayerId;
+ const config = lizMap.config;
+
+ // parent and current layer should be configured in relations object
+ if (!(parentLayerId in config.relations) || !(this.layerId in config.relations) || !this._parentFeatureId){
+ return null;
+ }
+
+ // pivot contains features?
+ const features = config.layers[this._pivotType]['features'];
+ if (!features || Object.keys(features).length <= 0){
+ return null;
+ }
+ // get pivot primary key
+ const primaryKey = this.pivotAttributeTableConfig?.['primaryKey'];
+ if(!primaryKey){
+ return null;
+ }
+
+ //get referencing field for the pivot
+ const layerReferencingField = config.relations[this.layerId].filter((rel)=>{
+ return rel.referencingLayer == pivotLayerId && rel.referencingField == config.relations.pivot[pivotLayerId][this.layerId]
+ })?.[0]?.referencingField;
+
+ const parentLayerReferencingField = config.relations[this.parentLayerId].filter((rel)=>{
+ return rel.referencingLayer == pivotLayerId && rel.referencingField == config.relations.pivot[pivotLayerId][this.parentLayerId]
+ })?.[0]?.referencingField;
+
+ if (!layerReferencingField || !parentLayerReferencingField) return null;
+
+ // get features from pivot corresponding to the current layer
+ const pivotFeature = Object.keys(features).filter((feat) =>{
+ const properties = features[feat].properties;
+ return properties && properties[layerReferencingField] && properties[layerReferencingField] == this.fid && properties[parentLayerReferencingField] && properties[parentLayerReferencingField] == this._parentFeatureId
+ })
+
+ if (pivotFeature.length == 1 && features[pivotFeature[0]].properties[primaryKey]) {
+ return features[pivotFeature[0]].properties[primaryKey]
+ } else return null
}
/**
@@ -209,8 +279,8 @@ export default class FeatureToolbar extends HTMLElement {
*/
get isDeletable(){
return this._isFeatureEditable
- && lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.deleteFeature === "True"
- && !this.isLayerPivot;
+ && ((lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.deleteFeature === "True"
+ && !this.isNToMRelation) || (this.isNToMRelation && lizMap.config?.editionLayers?.[this.featureType]?.capabilities?.deleteFeature === "True" && lizMap.config?.editionLayers?.[this._pivotType]?.capabilities?.deleteFeature === "True"));
}
get hasEditionRestricted(){
@@ -280,7 +350,8 @@ export default class FeatureToolbar extends HTMLElement {
// Check if the child layer has insert capabilities
let [childFeatureType, childLayerConfig] = lizMap.getLayerConfigById(relation.referencingLayer);
- if (lizMap.config?.editionLayers?.[childFeatureType]?.capabilities?.createFeature !== "True") {
+ let isPivot = !!lizMap.config?.relations?.pivot?.[relation.referencingLayer]
+ if (isPivot || lizMap.config?.editionLayers?.[childFeatureType]?.capabilities?.createFeature !== "True") {
return;
}
editableChildrenLayers.push({
@@ -367,7 +438,46 @@ export default class FeatureToolbar extends HTMLElement {
}
delete(){
- lizMap.deleteEditionFeature(this.layerId, this.fid);
+ // get list of tables that are linked to the pivot
+ let relations = lizMap.config?.relations?.[this.layerId], message = "";
+ if(relations && lizMap.config?.relations?.pivot){
+
+ let pivotNames = relations.map((relation)=>{
+ return relation.referencingLayer
+ }).filter((refLayer)=>{
+ const attributeTableConf = lizMap.getLayerConfigById(refLayer, lizMap.config.attributeLayers,'layerId')
+ return attributeTableConf && attributeTableConf[1]?.pivot == 'True' && refLayer && refLayer in lizMap.config.relations.pivot && Object.keys(lizMap.config.relations.pivot[refLayer]).some((kp)=>{return kp == this.layerId})
+ }).map((key)=>{
+ let relatedLayerId = Object.keys(lizMap.config.relations.pivot[key]).filter((k)=> { return k !== this.layerId})?.[0]
+ if (relatedLayerId) {
+ return lizMap.getLayerConfigById(relatedLayerId)?.[1]?.title || lizMap.getLayerConfigById(relatedLayerId)?.[1]?.name
+ }
+ else return "";
+ }).reduce((acc,current)=> acc+"\n" +current,"")
+
+ if (pivotNames) {
+ message = lizDict['edition.confirm.pivot.delete'].replace('%s',pivotNames);
+ }
+ }
+
+ lizMap.deleteEditionFeature(this.layerId, this.fid, message);
+ }
+
+ deleteFromPivot(){
+ let pivotFeatureId = this.pivotFeatureId;
+ if( pivotFeatureId ){
+ let unlinkMessage = lizDict['edition.confirm.pivot.unlink'].replace("%l", lizMap.getLayerConfigById(this.parentLayerId)[1].title)
+ lizMap.deleteEditionFeature(this.pivotLayerId, pivotFeatureId, unlinkMessage, ()=>{
+ // refresh mlayer
+ lizMap.events.triggerEvent("lizmapeditionfeaturedeleted",
+ {
+ 'layerId': this.layerId,
+ 'featureId': this.fid,
+ 'featureType': this.featureType,
+ 'updateDrawing': true
+ });
+ });
+ }
}
unlink(){
diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js
index 21af8de8d7..0a99fa04f3 100644
--- a/assets/src/legacy/attributeTable.js
+++ b/assets/src/legacy/attributeTable.js
@@ -183,7 +183,7 @@ var lizAttributeTable = function() {
const tableSelector = '#attribute-layer-table-' + cleanName;
// Get data and fill attribute table
- getDataAndFillAttributeTable(layerName, layerFilter, tableSelector);
+ getDataAndFillAttributeTable(layerName, layerFilter, tableSelector, false);
$('#nav-tab-attribute-layer-' + cleanName + ' a' ).tab('show');
@@ -279,9 +279,10 @@ var lizAttributeTable = function() {
* @param layerName
* @param filter
* @param tableSelector
+ * @param forceEmptyTable
* @param callBack
*/
- function getDataAndFillAttributeTable(layerName, filter, tableSelector, callBack){
+ function getDataAndFillAttributeTable(layerName, filter, tableSelector, forceEmptyTable, callBack){
let layerConfig = lizMap.config.layers[layerName];
const typeName = layerConfig.typename;
@@ -344,6 +345,7 @@ var lizAttributeTable = function() {
}));
}
}
+ if (forceEmptyTable) return buildLayerAttributeDatatable(layerName, tableSelector, [], layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack);
document.body.style.cursor = 'progress';
Promise.all(fetchRequests).then(responses => {
@@ -677,7 +679,7 @@ var lizAttributeTable = function() {
const tableSelector = '#attribute-layer-table-'+cleanName;
$('#attribute-layer-main-'+cleanName+' > div.attribute-layer-content').hide();
- getDataAndFillAttributeTable(lname, null, tableSelector, () => {
+ getDataAndFillAttributeTable(lname, null, tableSelector, false, () => {
$('#attribute-layer-main-' + cleanName + ' > div.attribute-layer-content').show();
refreshDatatableSize('#attribute-layer-main-' + cleanName);
});
@@ -801,9 +803,10 @@ var lizAttributeTable = function() {
var parentLayerName = attributeLayersDic[ cleanName ];
var parentLayerId = config.layers[parentLayerName]['id'];
var aName = attributeLayersDic[ $(this).val() ];
+ var pivotId = $(this).attr("data-pivot");
lizMap.getLayerFeature(parentLayerName, parentFeatId, function(parentFeat) {
var lid = config.layers[aName]['id'];
- lizMap.launchEdition( lid, null, {layerId:parentLayerId,feature:parentFeat});
+ lizMap.launchEdition( lid, null, {layerId:parentLayerId, feature:parentFeat, pivotId:pivotId});
});
return false;
})
@@ -923,7 +926,19 @@ var lizAttributeTable = function() {
lizMap.events.triggerEvent("layerfeatureunselectall",
{ 'featureType': attributeLayersDic[cleanName], 'updateDrawing': true}
);
- // Send signal saying edition has been done on pivot
+ // Send signal saying edition has been done on pivot and refresh corresponding tables
+ var linkedId = lizMap.config.layers[attributeLayersDic[cleanName]]?.id
+ if (linkedId) {
+ var pivotCfg = lizMap.config.relations.pivot[cId];
+ // get layerId of related layer
+ var linkedKey = Object.keys(pivotCfg).filter((key)=>{
+ return key != linkedId
+ })?.[0]
+
+ if (linkedKey) {
+ cId = linkedKey;
+ }
+ }
lizMap.events.triggerEvent("lizmapeditionfeaturecreated",
{ 'layerId': cId}
);
@@ -1024,19 +1039,30 @@ var lizAttributeTable = function() {
var childActive = 'active';
for( var lid in layerRelations ) {
var relation = layerRelations[lid];
- var childLayerConfigA = lizMap.getLayerConfigById(
+ var referencingLayerConfig = lizMap.getLayerConfigById(
relation.referencingLayer,
config.layers,
'id'
);
- if( childLayerConfigA
- && childLayerConfigA[0] in config.attributeLayers
- ){
+ var isNToM = false;
+ var pivotConfig = null;
+ var mLayerConfig = null;
+
+ if (referencingLayerConfig && referencingLayerConfig[0] in config.attributeLayers) {
+ // check if the renferencing layer is a pivot
+ mLayerConfig = getPivotLinkedLayerConfiguration(parentLayerId, referencingLayerConfig[1]);
+ // if so, switch the child layer to the "n_layer" if the n_layer could be displayed in attribute table
+ if( mLayerConfig && mLayerConfig.config && mLayerConfig.config[0] in config.attributeLayers) {
+ // store original pivot configuration
+ pivotConfig = referencingLayerConfig;
+ isNToM = true;
+ }
childCount+=1;
if( childCount > 1)
childActive = '';
- var childLayerConfig = childLayerConfigA[1];
- var childLayerName = childLayerConfigA[0];
+ // if the detected relation is n to m, then use mLayer configuration to display the child attribute table
+ var childLayerConfig = isNToM ? mLayerConfig.config[1] : referencingLayerConfig[1];
+ var childLayerName = isNToM ? mLayerConfig.config[0] : referencingLayerConfig[0];
var childAttributeLayerConfig = config.attributeLayers[childLayerName];
// Discard if the editor does not want this layer to be displayed in child table
@@ -1063,29 +1089,28 @@ var lizAttributeTable = function() {
// Add create child feature button
var canCreateChild = false;
if( 'editionLayers' in config ){
- var editionConfig = lizMap.getLayerConfigById(
- relation.referencingLayer,
- config.editionLayers,
- 'layerId'
- );
if( childLayerName in config.editionLayers ) {
var al = config.editionLayers[childLayerName];
if( al.capabilities.createFeature == "True" )
canCreateChild = true;
}
+ // if the m layer is displayed then check also the edition capabilities on pivot
+ if(canCreateChild && isNToM){
+ // check edition capabilities for pivot table
+ canCreateChild = pivotConfig[0] in config.editionLayers && config.editionLayers[pivotConfig[0]] && config.editionLayers[pivotConfig[0]].capabilities.createFeature == 'True'
+ }
}
-
if( canCreateChild ){
// Add a button to create a new feature for this child layer
let childButtonItem = `
-
+
➕ ${childLayerConfig.title}
`;
childCreateButtonItems.push(childButtonItem);
// Link parent with the selected features of the child
- layerLinkButtonItems.push('' + childLayerConfig.title +' ' );
+ layerLinkButtonItems.push('' + (isNToM ? pivotConfig[1].title : childLayerConfig.title) +' ' );
}
}
}
@@ -1147,28 +1172,41 @@ var lizAttributeTable = function() {
if( 'relations' in config && parentLayerId in config.relations) {
var layerRelations = config.relations[parentLayerId];
for (const relation of layerRelations ) {
- const childLayerConfigA = lizMap.getLayerConfigById(
+ var referencingLayerConfig = lizMap.getLayerConfigById(
relation.referencingLayer,
config.layers,
'id'
);
// Fill in attribute table for child
- // Discard if the editor does not want this layer to be displayed in child table
- if( childLayerConfigA
- && config.attributeLayers?.[childLayerConfigA[0]]?.['hideAsChild'] == 'False'
- ){
- const [childLayerName, childLayerConfig] = childLayerConfigA;
- // Generate filter
- let filter = '';
- if( relation.referencingLayer == childLayerConfig.id ){
- filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'";
+ if( referencingLayerConfig ) {
+ var isNToM = false, mLayerConfig = null;
+ // check if the referencingLayer is a pivot table
+ mLayerConfig = getPivotLinkedLayerConfiguration(parentLayerId, referencingLayerConfig[1]);
+ if (mLayerConfig) {
+ // if the realtion is n to m, switch the layer config to the mLayer
+ referencingLayerConfig = mLayerConfig.config;
+ isNToM = true;
+ }
+ // Discard if the editor does not want this layer to be displayed in child table
+ if (config.attributeLayers?.[referencingLayerConfig[0]]?.['hideAsChild'] == 'False') {
+ const [childLayerName, childLayerConfig] = referencingLayerConfig;
+ // Get child table id
+ const childTableSelector = sourceTable.replace(' table:first', '') + '-' + lizMap.cleanName(childLayerName);
+ // Generate filter
+ let filter = '';
+ if ( isNToM ) {
+ // get feature from pivot
+ getPivotWFSFeatures(relation.referencingLayer, mLayerConfig.relation, fp[relation.referencedField]).then((filterString)=>{
+ getEditionChildData(childLayerName, filterString, childTableSelector, filterString ? false : true);
+ })
+ } else {
+ if( relation.referencingLayer == childLayerConfig.id ){
+ filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'";
+ }
+ getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false);
+ }
}
-
- // Get child table id
- const childTableSelector = sourceTable.replace(' table:first', '') + '-' + lizMap.cleanName(childLayerName);
-
- getDataAndFillAttributeTable(childLayerName, filter, childTableSelector);
}
}
}
@@ -1327,7 +1365,28 @@ var lizAttributeTable = function() {
){
isPivot = true;
}
-
+ var pivotReference = null;
+ // checks if the parent and child are related via pivot
+ if (parentLayerID) {
+ // means that the table is displayed as a child
+ var parentLayerConfig = lizMap.getLayerConfigById(parentLayerID);
+ var fromEditionForm = aTable.startsWith('#attribute-layer-table-') ? false : (aTable.startsWith('#edition-table-') ? true : false);
+ var highlightedFeature = null;
+ if (fromEditionForm) {
+ // get fid of current layer on editing
+ highlightedFeature = $('#edition-form-container form input[name="liz_featureId"]').val()
+
+ } else highlightedFeature = config.layers[parentLayerConfig[1].cleanname].highlightedFeature;
+
+ if (parentLayerConfig && parentLayerConfig[1] && parentLayerConfig[1].cleanname && highlightedFeature) {
+
+ var childLayerId = lConfig.id;
+ var pivotId = getPivotIdFromRelatedLayers(parentLayerID, childLayerId);
+ if (pivotId) {
+ pivotReference = pivotId + ":" + highlightedFeature;
+ }
+ }
+ }
// Hidden fields
var hiddenFields = [];
if( aName in config.attributeLayers
@@ -1375,8 +1434,8 @@ var lizAttributeTable = function() {
hiddenFields,
lConfig['selectedFeatures'],
lConfig['id'],
- parentLayerID
- );
+ parentLayerID,
+ pivotReference);
var foundFeatures = ff.foundFeatures;
var dataSet = ff.dataSet;
@@ -1516,6 +1575,7 @@ var lizAttributeTable = function() {
} else {
$(aTable).show();
+ refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'))
}
@@ -1735,8 +1795,9 @@ var lizAttributeTable = function() {
* @param selectedFeatures
* @param layerId
* @param parentLayerID
+ * @param pivotId
*/
- function formatDatatableFeatures(atFeatures, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID){
+ function formatDatatableFeatures(atFeatures, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId = null){
var dataSet = [];
var foundFeatures = {};
atFeatures.forEach(function(feat) {
@@ -1752,8 +1813,7 @@ var lizAttributeTable = function() {
if( selectedFeatures && $.inArray( fid, selectedFeatures ) != -1 )
line.lizSelected = 'a';
-
- line['featureToolbar'] = ` `;
+ line['featureToolbar'] = ` `;
// Build table lines
for (var idx in feat.properties){
@@ -1831,9 +1891,10 @@ var lizAttributeTable = function() {
* @param childLayerName
* @param filter
* @param childTable
+ * @param forceEmptyTable
*/
- function getEditionChildData( childLayerName, filter, childTable ){
- getDataAndFillAttributeTable(childLayerName, filter, childTable, () => {
+ function getEditionChildData( childLayerName, filter, childTable, forceEmptyTable = false ){
+ getDataAndFillAttributeTable(childLayerName, filter, childTable, forceEmptyTable, () => {
// Check edition capabilities
var canCreate = false;
var canEdit = false;
@@ -1872,7 +1933,10 @@ var lizAttributeTable = function() {
lizMap.getLayerFeature(parentLayerName, parentFeatId, function (parentFeat) {
var parentLayerId = config.layers[lizMap.getLayerNameByCleanName(parentLayerName)]['id'];
var lid = config.layers[lizMap.getLayerNameByCleanName(layerName)]['id'];
- lizMap.launchEdition(lid, null, { layerId: parentLayerId, feature: parentFeat });
+ // n to m relations check
+ var pivotId = getPivotIdFromRelatedLayers(lid, parentLayerId);
+
+ lizMap.launchEdition(lid, null, { layerId: parentLayerId, feature: parentFeat, pivotId: pivotId });
});
return false;
});
@@ -2942,13 +3006,51 @@ var lizAttributeTable = function() {
if ( (featureType in config.attributeLayers) && parentLayerName == getParentLayerConfig[0] ) {
// get featureType layer config
var featureTypeConfig = config.attributeLayers[featureType];
+
+ // n to m checks
+ var pivotId = getPivotIdFromRelatedLayers(formLayerId, featureTypeConfig.layerId)
+
//get relation
var relation = getRelationInfo(formLayerId,featureTypeConfig.layerId);
- if( relation != null ) {
+
+ if( relation != null || pivotId) {
lizMap.getLayerFeature(parentLayerName, formFeatureId, function(feat) {
var fp = feat.properties;
- filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'";
- getEditionChildData( featureType, filter, zTable);
+ if (pivotId) {
+ const currentPivot = config.relations.pivot[pivotId];
+ const pivotConfig = lizMap.getLayerConfigById(
+ pivotId,
+ config.layers,
+ 'id'
+ );
+ if( pivotConfig && pivotConfig[1] ){
+
+ const referencedPivotField = currentPivot[formLayerId];
+ const referencingPivotField = currentPivot[featureTypeConfig.layerId];
+ const referencedFieldForFilter = config.relations[featureTypeConfig.layerId].filter((fil)=>{
+ return fil.referencingLayer == pivotId
+ })[0]?.referencedField;
+
+ const childReferencedField = config.relations[featureTypeConfig.layerId].filter((rel)=>{
+ return rel.referencingLayer == pivotId
+ })[0]?.referencedField;
+
+ if(referencedPivotField && referencingPivotField && referencedFieldForFilter && childReferencedField){
+ const pWfsParam = {
+ referencedPivotField : referencedPivotField,
+ referencingPivotField : referencingPivotField,
+ referencedFieldForFilter : referencedFieldForFilter
+ }
+ getPivotWFSFeatures(pivotId, pWfsParam, fp[childReferencedField]).then((filterString)=>{
+ getEditionChildData(featureType, filterString, zTable, filterString ? false : true);
+ })
+ }
+ }
+ } else {
+ var filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'";
+ getEditionChildData( featureType, filter, zTable);
+ }
+
});
}
}
@@ -2961,7 +3063,7 @@ var lizAttributeTable = function() {
// Else refresh main table with no filter
else{
// If not pivot
- getDataAndFillAttributeTable(featureType, null, zTable);
+ getDataAndFillAttributeTable(featureType, null, zTable, false);
}
}
});
@@ -2992,6 +3094,144 @@ var lizAttributeTable = function() {
dtable.DataTable().tables().columns.adjust();
}
+ /**
+ *
+ * @param nlayerId
+ * @param referencingLayerConfig
+ */
+ function getPivotLinkedLayerConfiguration(nlayerId, referencingLayerConfig){
+ const refAttributeLayerConf = lizMap.getLayerConfigById( referencingLayerConfig.id, lizMap.config.attributeLayers, 'layerId' );
+
+ if (refAttributeLayerConf && refAttributeLayerConf[1]?.pivot == 'True' && config.relations?.pivot && referencingLayerConfig.id in config.relations.pivot && nlayerId in config.relations.pivot[referencingLayerConfig.id]){
+ // get referenced layer for the parent layer
+ const referencedLayer = Object.keys(config.relations.pivot[referencingLayerConfig.id]).filter((k)=>{ return k != nlayerId})
+ var mLayerConfig = null;
+ if (referencedLayer.length == 1) {
+ mLayerConfig = lizMap.getLayerConfigById(
+ referencedLayer[0],
+ config.layers,
+ 'id'
+ );
+
+ if (mLayerConfig) {
+ var currentPivot = config.relations.pivot[referencingLayerConfig.id];
+ if (currentPivot) {
+ const referencedPivotField = currentPivot[nlayerId];
+ const referencingPivotField = currentPivot[referencedLayer[0]];
+ const referencedFieldForFilter = config.relations[mLayerConfig[1].id].filter((fil)=>{
+ return fil.referencingLayer == referencingLayerConfig.id
+ })[0]?.referencedField;
+
+ if (referencedPivotField && referencingPivotField && referencedFieldForFilter) {
+ return {
+ config:mLayerConfig,
+ relation:{
+ referencedPivotField:referencedPivotField,
+ referencingPivotField:referencingPivotField,
+ referencedFieldForFilter:referencedFieldForFilter
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param nlayerId
+ * @param mLayerId
+ */
+ function getPivotIdFromRelatedLayers(nlayerId, mLayerId){
+ // returns the pivotId starting from the related layers
+ // this method assumes that the mLayer and nLayer is related with n to m relation via a single pivot
+ // i.e. there is not n to m relation duplication
+ if (config.relations.pivot) {
+ var pivotId = Object.keys(config.relations.pivot).filter((key)=>{
+ return config.relations.pivot[key][mLayerId] != null && config.relations.pivot[key][nlayerId] != null;
+ })[0] //<--- assumes that the couple father-childs belongs to a single pivot
+
+ if (pivotId) {
+ const refAttributeLayerConf = lizMap.getLayerConfigById( pivotId, lizMap.config.attributeLayers, 'layerId' );
+ if(refAttributeLayerConf && refAttributeLayerConf[1]?.pivot == 'True'){
+ // check if pivot is in relations for both layers
+ const validRelation = [nlayerId,mLayerId].every((layerId)=>{
+ return config.relations[layerId] && config.relations[layerId].filter((rlayer)=>{ return rlayer.referencingLayer == pivotId}).length == 1
+ })
+ if (validRelation)
+ return pivotId;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param pivotId
+ * @param wfsFields
+ * @param referencedFieldValue
+ */
+ async function getPivotWFSFeatures(pivotId, wfsFields, referencedFieldValue){
+
+ const pivotConfig = lizMap.getLayerConfigById(
+ pivotId,
+ config.layers,
+ 'id'
+ );
+
+ let filterString = "", feats = {};
+ if( pivotConfig && pivotConfig[1] ){
+ // the field on the pivot linked to the nLayer
+ const referencedPivotField = wfsFields.referencedPivotField;
+ // the field on the pivot linked to the mLayer
+ const referencingPivotField = wfsFields.referencingPivotField;
+ // the field on the mLayer linked the the pivot
+ const referencedFieldForFilter = wfsFields.referencedFieldForFilter;
+
+ const typeName = pivotConfig[1].typename;
+ const wfsParams = {
+ TYPENAME: typeName,
+ GEOMETRYNAME: 'extent'
+ };
+ wfsParams['EXP_FILTER'] = '"' + referencedPivotField + '" = ' + "'" + referencedFieldValue + "'";
+ if (config.options?.limitDataToBbox == 'True') {
+ wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent();
+ wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ }
+
+ const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams);
+
+ let results = await getFeatureRequest;
+
+ if(results && results.features){
+
+ const features = results.features;
+
+ let filArray = [];
+ features.forEach((feat)=>{
+ var fid = feat.id.split('.')[1];
+ feats[fid] = feat;
+ if (feat.properties && feat.properties[referencingPivotField]) {
+ filArray.push(feat.properties[referencingPivotField])
+ }
+ })
+ if (filArray.length) {
+ var fil = filArray.map(function(val){
+ return '"'+referencedFieldForFilter+'" = \''+val+'\'';
+ })
+ filterString = fil.join(" OR ");
+ }
+ }
+
+ }
+ pivotConfig[1].features = feats;
+
+ return filterString;
+ }
+
lizMap.refreshDatatableSize = function(container){
return refreshDatatableSize(container);
}
@@ -3223,9 +3463,10 @@ var lizAttributeTable = function() {
}
var parentLayerId = layerId;
var aName = attributeLayersDic[ $(this).val() ];
+ var pivotId = $(this).attr("data-pivot");
lizMap.getLayerFeature(featureType, fid, function(parentFeat) {
var lid = config.layers[aName]['id'];
- lizMap.launchEdition( lid, null, {layerId:parentLayerId, feature:parentFeat});
+ lizMap.launchEdition( lid, null, {layerId:parentLayerId, feature:parentFeat, pivotId: pivotId});
});
return false;
})
@@ -3260,16 +3501,32 @@ var lizAttributeTable = function() {
var rLayerId = r.referencingLayer;
var rGetLayerConfig = lizMap.getLayerConfigById( rLayerId );
if ( rGetLayerConfig ) {
- var rLayerName = rGetLayerConfig[0];
- var rConfigLayer = rGetLayerConfig[1];
- var filter = '"' + r.referencingField + '" = ' + "'" + fp[r.referencedField] + "'";
- // Get child table id
- var parent_and_child = lizMap.cleanName(featureType) + '-' + lizMap.cleanName(rLayerName);
- var childTable = '#edition-table-' + parent_and_child;
+ // check if relation is nToM
+ var isNToM = false, mLayerConfig = null;
+ // check if the referencingLayer is a pivot table
+ mLayerConfig = getPivotLinkedLayerConfiguration(layerId, rGetLayerConfig[1]);
+ if (mLayerConfig) {
+ // if the realtion is n to m, switch the layer config to the mLayer
+ rGetLayerConfig = mLayerConfig.config;
+ isNToM = true;
+ }
+ let rLayerName = rGetLayerConfig[0];
+ let rConfigLayer = rGetLayerConfig[1];
+ let filter = "";
+ // Get child table id
+ let parent_and_child = lizMap.cleanName(featureType) + '-' + lizMap.cleanName(rLayerName);
+ let childTable = '#edition-table-' + parent_and_child;
// Fill in attribute table for child
- if( rLayerName in config.attributeLayers ) {
- getEditionChildData( rLayerName, filter, childTable );
+ if(rLayerName in config.attributeLayers){
+ if (isNToM) {
+ getPivotWFSFeatures(r.referencingLayer, mLayerConfig.relation, fp[r.referencedField]).then((filterString)=>{
+ getEditionChildData(rLayerName, filterString, childTable, filterString ? false : true);
+ })
+ } else {
+ filter = '"' + r.referencingField + '" = ' + "'" + fp[r.referencedField] + "'";
+ getEditionChildData( rLayerName, filter, childTable );
+ }
}
// Try to move the tables inside the parent form
diff --git a/assets/src/legacy/edition.js b/assets/src/legacy/edition.js
index e86e4a1a6b..8386b0d00d 100644
--- a/assets/src/legacy/edition.js
+++ b/assets/src/legacy/edition.js
@@ -35,6 +35,8 @@ var lizEdition = function() {
this.backToParent = false;
/** @member {[Feature, FormData][]} new features to save on submit (features created after a split) */
this.newfeatures = [];
+ /**@member {string} pivot tell if the parent is in n to m relation with the child*/
+ this.pivot = null;
}
FeatureEditionData.prototype = {
setParentToEditAfterSave: function (parent) {
@@ -602,14 +604,38 @@ var lizEdition = function() {
*
* @param parentLayerId
* @param childLayerId
+ * @param pivotId
*/
- function getRelationInfo(parentLayerId,childLayerId){
+ function getRelationInfo(parentLayerId, childLayerId, pivotId){
if( 'relations' in config && parentLayerId in config.relations) {
- var layerRelations = config.relations[parentLayerId];
- for( var lridx in layerRelations ) {
- var relation = layerRelations[lridx];
- if (relation.referencingLayer == childLayerId) {
- return relation;
+ if (pivotId) {
+ const pivotAttributeLayerConf = lizMap.getLayerConfigById( pivotId, lizMap.config.attributeLayers, 'layerId' );
+ if(pivotAttributeLayerConf && pivotAttributeLayerConf[1]?.pivot == 'True'){
+ var pivot = config.relations.pivot[pivotId]
+ if (pivot) {
+ // validate pivot reference
+ var validRelation = true;
+ Object.keys(pivot).forEach((k)=>{
+ if(validRelation){
+ if(k == parentLayerId || k == childLayerId){
+ var pvRelation = config.relations[k] || [];
+ var pr = pvRelation.filter((rel)=>{
+ return rel.referencingLayer == pivotId;
+ })
+ if(pr.length == 0) validRelation = false;
+ } else validRelation = false;
+ }
+ })
+ if(validRelation) return pivot;
+ }
+ }
+ } else {
+ var layerRelations = config.relations[parentLayerId];
+ for( var lridx in layerRelations ) {
+ var relation = layerRelations[lridx];
+ if (relation.referencingLayer == childLayerId) {
+ return relation;
+ }
}
}
}
@@ -1249,11 +1275,12 @@ var lizEdition = function() {
if (aParent != null && ('layerId' in aParent) && ('feature' in aParent)) {
var parentLayerId = aParent['layerId'];
var parentFeat = aParent['feature'];
+ var pivotId = aParent['pivotId']
if ('relations' in config &&
parentLayerId in config.relations) {
- var relation = getRelationInfo(parentLayerId, aLayerId);
- if (relation != null &&
- relation.referencingLayer == aLayerId
+ var relation = getRelationInfo(parentLayerId, aLayerId, pivotId);
+ if (relation != null && (pivotId ||
+ relation.referencingLayer == aLayerId)
) {
// the given parent information corresponds to a real parent
// of the feature we want to edit, we take care about it
@@ -1267,6 +1294,7 @@ var lizEdition = function() {
parentInfo = editionLayer.currentFeature;
parentInfo.relation = relation;
parentInfo.feature = parentFeat;
+ if (pivotId) parentInfo.pivot = pivotId;
editedFeature.setParentToEditAfterSave(parentInfo);
// and clear edition context
finishEdition();
@@ -1276,6 +1304,7 @@ var lizEdition = function() {
if (!parentInfo) {
// let's store parent data into a FeatureEditionData
parentInfo = new FeatureEditionData(parentLayerId, parentFeat, relation);
+ if (pivotId) parentInfo.pivot = pivotId;
editedFeature.parent = parentInfo;
}
}
@@ -1531,42 +1560,65 @@ var lizEdition = function() {
var relation = parentInfo['relation'];
var relationRefField = relation.referencingField;
var parentFeatProp = parentInfo['feature'].properties[relation.referencedField];
+ var pivot = parentInfo.pivot;
+ // link feature only when the user is creating a new feature (avoid duplicate associations on pivot)
+ if (pivot && editionType == 'createFeature') {
+ // get referencing field
+ var referencedField = config.relations[parentInfo.layerId].filter((rel) => {
+ return rel.referencingLayer == pivot;
+ })[0]?.referencedField
+ if (referencedField){
+ var parentLayerConf = lizMap.getLayerConfigById(parentInfo.layerId);
+ if(parentLayerConf[1]){
+ // add hidden input for manage feature link
+ var hiddenInput = $(' ')
+ .attr('id', pivot+'_hidden')
+ .attr('name', "liz_pivot")
+ .attr('value', pivot+":"+parentInfo.layerId+":"+parentInfo['feature'].properties[referencedField]);
+ form.find('div.jforms-hiddens').append(hiddenInput);
+ var futureLinkInfo = ''+lizDict['edition.link.pivot.add'].replace('%f',''+parentInfo['feature'].properties[referencedField]+' ').replace("%l",''+parentLayerConf[1].title+' ')+'
';
+ form.find(".control-group").last().append(futureLinkInfo);
+ }
- var select = form.find('select[name="'+relationRefField+'"]');
- if( select.length == 1 ){
- // Disable the select, the value will be stored in an hidden input
- select.val(parentFeatProp)
- .attr('disabled','disabled');
- // Create hidden input to store value because the select is disabled
- var hiddenInput = $(' ')
- .attr('id', select.attr('id')+'_hidden')
- .attr('name', relationRefField)
- .attr('value', parentFeatProp);
- form.find('div.jforms-hiddens').append(hiddenInput);
- // Disable required constraint
- jFormsJQ.getForm(form.attr('id'))
- .getControl(relationRefField)
- .required=false;
+ }
} else {
- var input = form.find('input[name="'+relationRefField+'"]');
- if( input.length == 1 && input.attr('type') != 'hidden'){
+ var select = form.find('select[name="'+relationRefField+'"]');
+ if( select.length == 1 ){
// Disable the select, the value will be stored in an hidden input
- input.val(parentFeatProp)
+ select.val(parentFeatProp)
.attr('disabled','disabled');
// Create hidden input to store value because the select is disabled
var hiddenInput = $(' ')
- .attr('id', input.attr('id')+'_hidden')
+ .attr('id', select.attr('id')+'_hidden')
.attr('name', relationRefField)
.attr('value', parentFeatProp);
form.find('div.jforms-hiddens').append(hiddenInput);
// Disable required constraint
- jFormsJQ.getForm($('#edition-form-container form').attr('id'))
+ jFormsJQ.getForm(form.attr('id'))
.getControl(relationRefField)
.required=false;
+ } else {
+ var input = form.find('input[name="'+relationRefField+'"]');
+ if( input.length == 1 && input.attr('type') != 'hidden'){
+ // Disable the select, the value will be stored in an hidden input
+ input.val(parentFeatProp)
+ .attr('disabled','disabled');
+ // Create hidden input to store value because the select is disabled
+ var hiddenInput = $(' ')
+ .attr('id', input.attr('id')+'_hidden')
+ .attr('name', relationRefField)
+ .attr('value', parentFeatProp);
+ form.find('div.jforms-hiddens').append(hiddenInput);
+ // Disable required constraint
+ jFormsJQ.getForm($('#edition-form-container form').attr('id'))
+ .getControl(relationRefField)
+ .required=false;
+ }
+ else
+ input.val(parentFeatProp);
}
- else
- input.val(parentFeatProp);
}
+
}
// Create combobox based on RelationValue with fieldEditable
@@ -2173,9 +2225,23 @@ var lizEdition = function() {
if ( 'shortname' in configLayer && configLayer.shortname != '' )
typeName = configLayer.shortname;
+ // check if the layer has n to m relations
+ var hasNToMRelations = false, isPivot = !!lizMap.config?.relations?.pivot?.[aLayerId];
+ if(lizMap.config?.relations?.[aLayerId]){
+ hasNToMRelations = lizMap.config.relations[aLayerId].some((el)=>{
+ const pivotAttributeLayerConf = lizMap.getLayerConfigById( el.referencingLayer, lizMap.config.attributeLayers, 'layerId' );
+ return lizMap.config.relations.pivot && lizMap.config.relations.pivot[el.referencingLayer] != null && pivotAttributeLayerConf[1]?.pivot == 'True'
+ })
+ }
var deleteConfirm = lizDict['edition.confirm.delete'];
- if ( aMessage )
- deleteConfirm += '\n' + aMessage;
+
+ if(aMessage){
+ if(hasNToMRelations || isPivot){
+ deleteConfirm = aMessage;
+ } else {
+ deleteConfirm += '\n' + aMessage;
+ }
+ }
if ( !confirm( deleteConfirm ) )
return false;
@@ -2183,7 +2249,8 @@ var lizEdition = function() {
var eService = lizUrls.edition + '?' + new URLSearchParams(lizUrls.params);
$.get(eService.replace('getFeature','deleteFeature'),{
layerId: aLayerId,
- featureId: aFeatureId
+ featureId: aFeatureId,
+ linkedRecords: hasNToMRelations ? 'ntom' : ''
}, function(data){
addEditionMessage( data, 'info', true);
diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js
index 2ce39fddb3..7be63e2591 100644
--- a/assets/src/legacy/map.js
+++ b/assets/src/legacy/map.js
@@ -2088,178 +2088,309 @@ window.lizMap = function() {
popupMaxFeatures == 0 ? 10 : popupMaxFeatures;
getLayerFeature(featureType, fid, function(feat) {
- // Array of Promise w/ fetch to request children popup content
- const popupChidrenRequests = [];
- const rConfigLayerAll = [];
-
- // Build POST query for every child based on QGIS relations
- for ( const relation of relations ){
- const rLayerId = relation.referencingLayer;
- const rGetLayerConfig = getLayerConfigById( rLayerId );
- if ( rGetLayerConfig ) {
- const rConfigLayer = rGetLayerConfig[1];
- let clname = rConfigLayer?.shortname || rConfigLayer.cleanname;
- if ( clname === undefined ) {
- clname = cleanName(rConfigLayer.name);
- rConfigLayer.cleanname = clname;
- }
- if ( rConfigLayer.popup == 'True' && self.parent().find('div.lizmapPopupChildren.'+clname).length == 0) {
- let wmsName = rConfigLayer?.shortname || rConfigLayer.name;
- const wmsOptions = {
- 'LAYERS': wmsName
- ,'QUERY_LAYERS': wmsName
- ,'STYLES': ''
- ,'SERVICE': 'WMS'
- ,'VERSION': '1.3.0'
- ,'CRS': (('crs' in rConfigLayer) && rConfigLayer.crs != '') ? rConfigLayer.crs : 'EPSG:4326'
- ,'REQUEST': 'GetFeatureInfo'
- ,'EXCEPTIONS': 'application/vnd.ogc.se_inimage'
- ,'FEATURE_COUNT': popupMaxFeatures
- ,'INFO_FORMAT': 'text/html'
- };
-
- if ( 'popupMaxFeatures' in rConfigLayer && !isNaN(parseInt(rConfigLayer.popupMaxFeatures)) )
- wmsOptions['FEATURE_COUNT'] = parseInt(rConfigLayer.popupMaxFeatures);
- if ( wmsOptions['FEATURE_COUNT'] == 0 )
- wmsOptions['FEATURE_COUNT'] = popupMaxFeatures;
- if ( rConfigLayer.request_params && rConfigLayer.request_params.filter &&
- rConfigLayer.request_params.filter !== '' )
- wmsOptions['FILTER'] = rConfigLayer.request_params.filter+' AND "'+relation.referencingField+'" = \''+feat.properties[relation.referencedField]+'\'';
- else
- wmsOptions['FILTER'] = wmsName+':"'+relation.referencingField+'" = \''+feat.properties[relation.referencedField]+'\'';
-
- var parentDiv = self.parent();
-
- // Fetch queries
- // Keep `rConfigLayer` in array with same order that fetch queries
- // for later user when Promise.allSettled resolves
- rConfigLayerAll.push(rConfigLayer);
- popupChidrenRequests.push(
- fetch(lizUrls.service, {
- "method": "POST",
- "body": new URLSearchParams(wmsOptions)
- }).then(function (response) {
- return response.text();
- })
- );
- }
- }
- }
-
- // Fetch GetFeatureInfo query for every children popups
- Promise.allSettled(popupChidrenRequests).then(popupChildrenData => {
-
- const childPopupElements = [];
-
- for (let index = 0; index < popupChildrenData.length; index++) {
- let popupChildData = popupChildrenData[index].value;
-
- var hasPopupContent = (!(!popupChildData || popupChildData == null || popupChildData == ''))
- if (hasPopupContent) {
- var popupReg = new RegExp('lizmapPopupTable', 'g');
- popupChildData = popupChildData.replace(popupReg, 'table table-condensed table-striped lizmapPopupTable');
-
- const configLayer = rConfigLayerAll[index];
-
- var clname = configLayer.cleanname;
- if (clname === undefined) {
- clname = cleanName(configLayer.name);
- configLayer.cleanname = clname;
- }
-
- const resizeTablesButtons =
- ' '+
- ' ';
-
- var childPopup = $('');
-
- //Manage if the user choose to create a table for children
- if (['qgis', 'form'].indexOf(configLayer.popupSource) !== -1 &&
- childPopup.find('.lizmap_merged').length != 0) {
- // save inputs
- childPopup.find(".lizmapPopupDiv").each(function (i, e) {
- var popupDiv = $(e);
- if (popupDiv.find(".lizmapPopupHeader").prop("tagName") == 'TR') {
- popupDiv.find(".lizmapPopupHeader").prepend(" ");
- popupDiv.find(".lizmapPopupHeader").next().prepend(" ");
- } else {
- popupDiv.find(".lizmapPopupHeader").next().prepend(" ");
- }
- popupDiv.find(".lizmapPopupHeader").next().children().first().append(popupDiv.find("input"));
- });
-
- childPopup.find("h4").each(function (i, e) {
- if (i != 0)
- $(e).remove();
- });
-
- childPopup.find(".lizmapPopupHeader").each(function (i, e) {
- if (i != 0)
- $(e).remove();
- });
-
- childPopup.find(".lizmapPopupDiv").contents().unwrap();
- childPopup.find(".lizmap_merged").contents().unwrap();
- childPopup.find(".lizmapPopupDiv").remove();
- childPopup.find(".lizmap_merged").remove();
-
- childPopup.find(".lizmapPopupHidden").hide();
-
- var tChildPopup = $("");
- childPopup.append(tChildPopup);
- childPopup.find('tr').appendTo(tChildPopup);
-
- childPopup.children('tbody').remove();
- }
-
- var oldPopupChild = parentDiv.find('div.lizmapPopupChildren.' + clname);
- if (oldPopupChild.length != 0){
- oldPopupChild.remove();
- }
-
- parentDiv.append(childPopup);
-
- childPopupElements.push(childPopup);
-
- // Trigger event for single popup children
- lizMap.events.triggerEvent(
- "lizmappopupchildrendisplayed",
- { 'html': childPopup.html() }
- );
- }
- }
-
- // Handle compact-tables/explode-tables behaviour
- $('.lizmapPopupChildren .popupAllFeaturesCompact table').DataTable();
-
- $('.lizmapPopupChildren .compact-tables, .lizmapPopupChildren .explode-tables').tooltip();
-
- $('.lizmapPopupChildren .compact-tables').off('click').on('click',function() {
- $(this)
- .addClass('hide')
- .siblings('.explode-tables').removeClass('hide')
- .siblings('.popupAllFeaturesCompact, .lizmapPopupSingleFeature').toggle();
- });
-
- $('.lizmapPopupChildren .explode-tables').off('click').on('click',function () {
- $(this)
- .addClass('hide')
- .siblings('.compact-tables').removeClass('hide')
- .siblings('.popupAllFeaturesCompact, .lizmapPopupSingleFeature').toggle();
- });
-
- // Trigger event for all popup children
- lizMap.events.triggerEvent(
- "lizmappopupallchildrendisplayed",
- {
- parentPopupElement: self.parents('.lizmapPopupSingleFeature'),
- childPopupElements: childPopupElements
- }
- );
- });
- });
- });
- }
+ // Array of Promise w/ fetch to request children popup content
+ const popupChidrenRequests = [];
+
+ // Array of pre-processed objects for WMS popup requests
+ const preProcessedRequests = [];
+
+ // Array of object contains utilities for each relation
+ const preProcessUtilities = [];
+
+ const rConfigLayerAll = [];
+
+ // Build POST query for every child based on QGIS relations
+ for ( const relation of relations ){
+ const rLayerId = relation.referencingLayer;
+ let preProcessRequest = null;
+
+ // prepare utilities object
+ let rUtilities = {
+ rLayerId : rLayerId // pivot id or table id
+ };
+ const pivotAttributeLayerConf = lizMap.getLayerConfigById( rLayerId, lizMap.config.attributeLayers, 'layerId' );
+ // check if child is a pivot table
+ if (pivotAttributeLayerConf && pivotAttributeLayerConf[1]?.pivot == 'True' && config.relations.pivot && config.relations.pivot[rLayerId]) {
+ // looking for related children
+ const pivotConfig = lizMap.getLayerConfigById(
+ rLayerId,
+ config.layers,
+ 'id'
+ );
+ if (pivotConfig) {
+ // n to m -> get "m" layer id
+ var mLayer = Object.keys(config.relations.pivot[rLayerId]).filter((k)=>{ return k !== layerId})
+ if (mLayer.length == 1) {
+ // "m" layer config
+ const mLayerConfig = getLayerConfigById( mLayer[0] );
+ if (mLayerConfig) {
+ let clRefname = mLayerConfig[1]?.shortname || mLayerConfig[1]?.cleanname;
+ if ( clRefname === undefined ) {
+ clRefname = cleanName(mLayerConfig[1].name);
+ mLayerConfig[1].cleanname = clRefname;
+ }
+ if (mLayerConfig[1].popup == 'True' && self.parent().find('div.lizmapPopupChildren.'+clRefname).length == 0) {
+ // get results from pivot table
+ const typeName = pivotConfig[1].typename;
+ const wfsParams = {
+ TYPENAME: typeName,
+ GEOMETRYNAME: 'extent'
+ };
+
+ wfsParams['EXP_FILTER'] = '"' + config.relations.pivot[rLayerId][layerId] + '" = ' + "'" + feat.properties[relation.referencedField] + "'";;
+ // Calculate bbox
+ if (config.options?.limitDataToBbox == 'True') {
+ wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent();
+ wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ }
+ preProcessRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams);
+
+ let ut = {
+ pivotTableId: rLayerId,
+ mLayerConfig: mLayerConfig
+ }
+ rUtilities = {...rUtilities,...ut};
+ }
+ }
+ }
+ }
+ } else {
+ // one to n relation
+ const rGetLayerConfig = getLayerConfigById( rLayerId );
+ if ( rGetLayerConfig ) {
+ preProcessRequest = {
+ oneToN:true,
+ layer:rGetLayerConfig[1]
+ }
+ let ut = {
+ referencingField: relation.referencingField,
+ referencedField: relation.referencedField
+ }
+ rUtilities = {...rUtilities, ...ut}
+ }
+ }
+ preProcessedRequests.push(preProcessRequest);
+ preProcessUtilities.push(rUtilities)
+ }
+
+ Promise.allSettled(preProcessedRequests).then(preProcessResponses =>{
+ for (let rr = 0; rr < preProcessResponses.length; rr++) {
+ const resp = preProcessResponses[rr];
+ const utilities = preProcessUtilities[rr];
+ if (resp.value) {
+ const respValue = resp.value;
+ var confLayer = null, wmsFilter = null;
+ if (respValue.oneToN && utilities.referencingField && utilities.referencedField) {
+ confLayer = respValue.layer;
+ wmsFilter = '"'+utilities.referencingField+'" = \''+feat.properties[utilities.referencedField]+'\'';
+ } else {
+ if (respValue.features) {
+ const features = respValue.features;
+ const referencedFieldForFilter = config.relations[utilities.mLayerConfig[1].id].filter((fil)=>{
+ return fil.referencingLayer == utilities.rLayerId
+ })[0]?.referencedField;
+ let filArray = [];
+ const feats = {};
+ features.forEach((feat)=>{
+ var fid = feat.id.split('.')[1];
+ feats[fid] = feat;
+ if (feat.properties && feat.properties[config.relations.pivot[utilities.rLayerId][utilities.mLayerConfig[1].id]]) {
+ filArray.push(feat.properties[config.relations.pivot[utilities.rLayerId][utilities.mLayerConfig[1].id]])
+ }
+ })
+
+ if (filArray.length) {
+ let fil = filArray.map(function(val){
+ return '"'+referencedFieldForFilter+'" = \''+val+'\'';
+ })
+
+ wmsFilter = fil.join(" OR ");
+ }
+ const pivotConfig = lizMap.getLayerConfigById(
+ utilities.pivotTableId,
+ config.layers,
+ 'id'
+ );
+ pivotConfig[1].features = feats;
+ // get feature of mLayer
+ confLayer = utilities.mLayerConfig[1];
+ }
+ }
+ if (wmsFilter && confLayer) {
+ const rConfigLayer = confLayer;
+ let clname = rConfigLayer?.shortname || rConfigLayer.cleanname;
+ if ( clname === undefined ) {
+ clname = cleanName(rConfigLayer.name);
+ rConfigLayer.cleanname = clname;
+ }
+ if ( rConfigLayer.popup == 'True' && self.parent().find('div.lizmapPopupChildren.'+clname).length == 0) {
+ let wmsName = rConfigLayer?.shortname || rConfigLayer.name;
+ const wmsOptions = {
+ 'LAYERS': wmsName
+ ,'QUERY_LAYERS': wmsName
+ ,'STYLES': ''
+ ,'SERVICE': 'WMS'
+ ,'VERSION': '1.3.0'
+ ,'CRS': (('crs' in rConfigLayer) && rConfigLayer.crs != '') ? rConfigLayer.crs : 'EPSG:4326'
+ ,'REQUEST': 'GetFeatureInfo'
+ ,'EXCEPTIONS': 'application/vnd.ogc.se_inimage'
+ ,'FEATURE_COUNT': popupMaxFeatures
+ ,'INFO_FORMAT': 'text/html'
+ };
+ if ( 'popupMaxFeatures' in rConfigLayer && !isNaN(parseInt(rConfigLayer.popupMaxFeatures)) )
+ wmsOptions['FEATURE_COUNT'] = parseInt(rConfigLayer.popupMaxFeatures);
+ if ( wmsOptions['FEATURE_COUNT'] == 0 )
+ wmsOptions['FEATURE_COUNT'] = popupMaxFeatures;
+ if ( rConfigLayer.request_params && rConfigLayer.request_params.filter &&
+ rConfigLayer.request_params.filter !== '' )
+ wmsOptions['FILTER'] = rConfigLayer.request_params.filter+' AND '+wmsFilter;
+ else
+ wmsOptions['FILTER'] = wmsName+':'+wmsFilter;
+
+ var parentDiv = self.parent();
+ // Fetch queries
+ // Keep `rConfigLayer` in array with same order that fetch queries
+ // for later user when Promise.allSettled resolves
+ rConfigLayerAll.push(rConfigLayer);
+ popupChidrenRequests.push(
+ fetch(lizUrls.service, {
+ "method": "POST",
+ "body": new URLSearchParams(wmsOptions)
+ }).then(function (response) {
+ return response.text();
+ }).then( function (textResp) {
+ // add utilities object to response for further controls
+ return {
+ popupChildData:textResp,
+ utilities:utilities
+ }
+ })
+ );
+ }
+ }
+ }
+ }
+
+ // Fetch GetFeatureInfo query for every children popups
+ Promise.allSettled(popupChidrenRequests).then(popupChildrenData => {
+
+ const childPopupElements = [];
+
+ for (let index = 0; index < popupChildrenData.length; index++) {
+ let popupResponse = popupChildrenData[index].value;
+ let popupChildData = popupResponse.popupChildData;
+ const utilities = popupResponse.utilities;
+ var hasPopupContent = (!(!popupChildData || popupChildData == null || popupChildData == ''))
+ if (hasPopupContent) {
+ var popupReg = new RegExp('lizmapPopupTable', 'g');
+ popupChildData = popupChildData.replace(popupReg, 'table table-condensed table-striped lizmapPopupTable');
+
+ const configLayer = rConfigLayerAll[index];
+
+ var clname = configLayer.cleanname;
+ if (clname === undefined) {
+ clname = cleanName(configLayer.name);
+ configLayer.cleanname = clname;
+ }
+
+ if(utilities.pivotTableId){
+ var popupFeatureToolbarReg = new RegExp(' '+
+ ' ';
+
+ var childPopup = $('
');
+
+ // Manage if the user choose to create a table for children
+ if (['qgis', 'form'].indexOf(configLayer.popupSource) !== -1 && childPopup.find('.lizmap_merged').length != 0) {
+ // save inputs
+ childPopup.find(".lizmapPopupDiv").each(function (i, e) {
+ var popupDiv = $(e);
+ if (popupDiv.find(".lizmapPopupHeader").prop("tagName") == 'TR') {
+ popupDiv.find(".lizmapPopupHeader").prepend(" ");
+ popupDiv.find(".lizmapPopupHeader").next().prepend(" ");
+ } else {
+ popupDiv.find(".lizmapPopupHeader").next().prepend(" ");
+ }
+ popupDiv.find(".lizmapPopupHeader").next().children().first().append(popupDiv.find("input"));
+ });
+ childPopup.find("h4").each(function (i, e) {
+ if (i != 0)
+ $(e).remove();
+ });
+
+ childPopup.find(".lizmapPopupHeader").each(function (i, e) {
+ if (i != 0)
+ $(e).remove();
+ });
+
+ childPopup.find(".lizmapPopupDiv").contents().unwrap();
+ childPopup.find(".lizmap_merged").contents().unwrap();
+ childPopup.find(".lizmapPopupDiv").remove();
+ childPopup.find(".lizmap_merged").remove();
+
+ childPopup.find(".lizmapPopupHidden").hide();
+
+ var tChildPopup = $("");
+ childPopup.append(tChildPopup);
+ childPopup.find('tr').appendTo(tChildPopup);
+
+ childPopup.children('tbody').remove();
+ }
+
+ var oldPopupChild = parentDiv.find('div.lizmapPopupChildren.' + clname);
+ if (oldPopupChild.length != 0) {
+ oldPopupChild.remove();
+ }
+
+ parentDiv.append(childPopup);
+
+ childPopupElements.push(childPopup);
+
+ // Trigger event for single popup children
+ lizMap.events.triggerEvent(
+ "lizmappopupchildrendisplayed",
+ { 'html': childPopup.html() }
+ );
+ }
+ }
+
+ // Handle compact-tables/explode-tables behaviour
+ $('.lizmapPopupChildren .popupAllFeaturesCompact table').DataTable();
+
+ $('.lizmapPopupChildren .compact-tables, .lizmapPopupChildren .explode-tables').tooltip();
+
+ $('.lizmapPopupChildren .compact-tables').off('click').on('click',function() {
+ $(this)
+ .addClass('hide')
+ .siblings('.explode-tables').removeClass('hide')
+ .siblings('.popupAllFeaturesCompact, .lizmapPopupSingleFeature').toggle();
+ });
+
+ $('.lizmapPopupChildren .explode-tables').off('click').on('click',function () {
+ $(this)
+ .addClass('hide')
+ .siblings('.compact-tables').removeClass('hide')
+ .siblings('.popupAllFeaturesCompact, .lizmapPopupSingleFeature').toggle();
+ });
+
+ // Trigger event for all popup children
+ lizMap.events.triggerEvent(
+ "lizmappopupallchildrendisplayed",
+ {
+ parentPopupElement: self.parents('.lizmapPopupSingleFeature'),
+ childPopupElements: childPopupElements
+ }
+ );
+ });
+ })
+ });
+ });
+ }
/**
*
diff --git a/lizmap/modules/lizmap/classes/qgisVectorLayer.class.php b/lizmap/modules/lizmap/classes/qgisVectorLayer.class.php
index e566b2f3f5..2cb9a664f0 100644
--- a/lizmap/modules/lizmap/classes/qgisVectorLayer.class.php
+++ b/lizmap/modules/lizmap/classes/qgisVectorLayer.class.php
@@ -949,21 +949,26 @@ public function updateFeature($feature, $values, $loginFilteredLayers)
}
/**
- * @param object $feature
- * @param null|array $loginFilteredLayers array with these keys:
- * - where: SQL WHERE statement
- * - type: 'groups' or 'login'
- * - attribute: filter attribute from the layer
+ * @param object $feature
+ * @param null|array $loginFilteredLayers array with these keys:
+ * - where: SQL WHERE statement
+ * - type: 'groups' or 'login'
+ * - attribute: filter attribute from the layer
+ * @param null|jDbConnection $connection DBConnection, if not null then the parameter conneciton is used, default value null
*
* @throws Exception
*
* @return int
*/
- public function deleteFeature($feature, $loginFilteredLayers)
+ public function deleteFeature($feature, $loginFilteredLayers, $connection = null)
{
// Get database connection object
$dtParams = $this->getDatasourceParameters();
- $cnx = $this->getDatasourceConnection();
+ if ($connection) {
+ $cnx = $connection;
+ } else {
+ $cnx = $this->getDatasourceConnection();
+ }
$dbFieldsInfo = $this->getDbFieldsInfo();
// SQL for deleting on line in the edition table
diff --git a/lizmap/modules/lizmap/controllers/edition.classic.php b/lizmap/modules/lizmap/controllers/edition.classic.php
index 86454564fb..ee7966c3f5 100644
--- a/lizmap/modules/lizmap/controllers/edition.classic.php
+++ b/lizmap/modules/lizmap/controllers/edition.classic.php
@@ -167,81 +167,138 @@ private function getEditionParameters($save = false)
return false;
}
+ $layerEditionParameters = $this->getLayerEditionParameter($lproj, $layerId, $featureIdParam);
- /** @var qgisVectorLayer $layer The QGIS vector layer instance */
- $layer = $lproj->getLayer($layerId);
-
- if (!$layer) {
- $this->setErrorMessage(jLocale::get('view~edition.message.error.layer.editable'), 'LayerNotEditable');
-
+ if (!$layerEditionParameters) {
return false;
}
- $layerXml = $layer->getXmlLayer();
- $layerName = $layer->getName();
- // Verifying if the layer is editable
- if (!$layer->isEditable()) {
- $this->setErrorMessage(jLocale::get('view~edition.message.error.layer.editable'), 'LayerNotEditable');
+ // Define class private properties
+ $this->project = $lproj;
+ $this->repository = $lrep;
+ $this->layerId = $layerId;
- return false;
+ $this->featureId = $layerEditionParameters['featureId'];
+ $this->featureIdParam = $layerEditionParameters['featureIdParam'];
+ $this->layer = $layerEditionParameters['layer'];
+ $this->layerXml = $layerEditionParameters['layerXml'];
+ $this->layerName = $layerEditionParameters['layerName'];
+
+ // Optionally filter data by login or/and by polygon
+ $this->loginFilteredOverride = jAcl2::check('lizmap.tools.loginFilteredLayers.override', $lrep->getKey());
+
+ $dbFieldsInfo = $this->layer->getDbFieldsInfo();
+ $this->primaryKeys = $layerEditionParameters['primaryKeys'];
+ $this->geometryColumn = $layerEditionParameters['geometryColumn'];
+ $this->srid = $layerEditionParameters['srid'];
+ $this->proj4 = $layerEditionParameters['proj4'];
+
+ return true;
+ }
+
+ /**
+ * Get layer parameters and returns layers info for edition.
+ *
+ * @param null|Lizmap\Project\Project $proj the project
+ * @param null|string $lid the layer id
+ * @param null|string|string[] $fIdParams the feature ids
+ * @param bool $setMessage set/not set error message
+ *
+ * @return null|array{'layer': qgisVectorLayer, 'layerXml': simpleXMLElement, 'layerName': string, 'featureId': mixed, 'featureIdParam': string, 'dbFieldsInfo': qgisLayerDbFieldsInfo, 'primaryKeys': string[], 'geometryColumn': string, 'srid': mixed, 'proj4': mixed}
+ */
+ protected function getLayerEditionParameter($proj, $lid, $fIdParams, $setMessage = true)
+ {
+ if (!$proj || !$lid) {
+ return null;
}
- if (!$layer->canCurrentUserEdit()) {
- $this->setErrorMessage(jLocale::get('view~edition.access.denied'), 'AuthorizationRequired');
+ /** @var null|qgisVectorLayer $player The QGIS vector layer instance */
+ $player = $proj->getLayer($lid);
- return false;
+ if (!$player) {
+ if ($setMessage) {
+ $this->setErrorMessage(jLocale::get('view~edition.message.error.layer.editable'), 'LayerNotEditable');
+ }
+
+ return null;
}
- // feature Id (optional, only for edition and save)
- $featureId = $featureIdParam;
- if ($featureIdParam) {
- if (strpos($featureIdParam, ',') !== false) {
- $featureId = preg_split('#,#', $featureIdParam);
- } elseif (strpos($featureIdParam, '@@') !== false) {
- $featureId = preg_split('#@@#', $featureIdParam);
+ $layerXml = $player->getXmlLayer();
+ $layerName = $player->getName();
+
+ // Verifying if the layer is editable
+ if (!$player->isEditable()) {
+ if ($setMessage) {
+ $this->setErrorMessage(jLocale::get('view~edition.message.error.layer.editable'), 'LayerNotEditable');
}
+
+ return null;
}
- // Define class private properties
- $this->project = $lproj;
- $this->repository = $lrep;
- $this->layerId = $layerId;
- $this->featureId = $featureId;
- $this->featureIdParam = $featureIdParam;
- $this->layer = $layer;
- $this->layerXml = $layerXml;
- $this->layerName = $layerName;
+ if (!$player->canCurrentUserEdit()) {
+ if ($setMessage) {
+ $this->setErrorMessage(jLocale::get('view~edition.access.denied'), 'AuthorizationRequired');
+ }
- // Optionally filter data by login or/and by polygon
- $this->loginFilteredOverride = jAcl2::check('lizmap.tools.loginFilteredLayers.override', $lrep->getKey());
+ return null;
+ }
- $dbFieldsInfo = $this->layer->getDbFieldsInfo();
- $this->primaryKeys = $dbFieldsInfo->primaryKeys;
- $this->geometryColumn = $dbFieldsInfo->geometryColumn;
- $this->srid = $this->layer->getSrid();
- $this->proj4 = $this->layer->getProj4();
+ // feature Id (optional, only for edition and save)
+ $featureId = $fIdParams;
+ if ($fIdParams) {
+ if (strpos($fIdParams, ',') !== false) {
+ $featureId = preg_split('#,#', $fIdParams);
+ } elseif (strpos($fIdParams, '@@') !== false) {
+ $featureId = preg_split('#@@#', $fIdParams);
+ }
+ }
- return true;
+ $dbFieldsInfo = $player->getDbFieldsInfo();
+ $primaryKeys = $dbFieldsInfo->primaryKeys;
+ $geometryColumn = $dbFieldsInfo->geometryColumn;
+ $srid = $player->getSrid();
+ $proj4 = $player->getProj4();
+
+ return array(
+ 'layer' => $player,
+ 'layerXml' => $layerXml,
+ 'layerName' => $layerName,
+ 'featureId' => $featureId,
+ 'featureIdParam' => $fIdParams,
+ 'dbFieldsInfo' => $dbFieldsInfo,
+ 'primaryKeys' => $primaryKeys,
+ 'geometryColumn' => $geometryColumn,
+ 'srid' => $srid,
+ 'proj4' => $proj4,
+ );
}
- /*
+ /**
* Get the WFS feature for the editing object
- * and set the controller property: featureData.
+ * and return featureData object.
*
* This method will always return an object if the feature exists in the layer
* even if there are some filters by login or by polygon !
* /!\ This is not the responsibility of this method to know if the user has the right to edit !
*
+ * @param null|qgisVectorLayer $layer the qgis vector layer
+ * @param mixed $featureId the value to search
+ * @param null|mixed $keys primary keys or attribute filter
+ * @param string $exp_filter filter for attributes
+ *
+ * @return mixed
*/
- private function getWfsFeature()
+ private function getWfsFeature($layer, $featureId, $keys, $exp_filter = null)
{
- $featureId = $this->featureId;
+ if (!$layer || !$keys) {
+ return null;
+ }
// Get features primary key field values corresponding to featureId(s)
if (!empty($featureId) || $featureId === 0 || $featureId === '0') {
- $typename = $this->layer->getShortName();
+ $typename = $layer->getShortName();
if (!$typename || $typename == '') {
- $typename = str_replace(' ', '_', $this->layer->getName());
+ $typename = str_replace(' ', '_', $layer->getName());
}
if (is_array($featureId)) {
// QGIS3 (at least <=3.4) doesn't support pk with multiple fields
@@ -252,17 +309,17 @@ private function getWfsFeature()
}
// We must give the fields used in the filters (featureid and exp_filter)
- $propertyName = array_merge(array(), $this->primaryKeys);
+ $propertyName = array_merge(array(), $keys);
if (!$this->loginFilteredOverride) {
// login filter
- $loginFilteredConfig = $this->project->getLoginFilteredConfig($this->layer->getName(), true);
+ $loginFilteredConfig = $this->project->getLoginFilteredConfig($layer->getName(), true);
if ($loginFilteredConfig && property_exists($loginFilteredConfig, 'filterAttribute')) {
$propertyName[] = $loginFilteredConfig->filterAttribute;
}
// polygon filter
- $polygonFilter = $this->project->getLayerPolygonFilterConfig($this->layer->getName(), true);
+ $polygonFilter = $this->project->getLayerPolygonFilterConfig($layer->getName(), true);
if ($polygonFilter && !in_array($polygonFilter['primary_key'], $propertyName)) {
$propertyName[] = $polygonFilter['primary_key'];
}
@@ -280,13 +337,18 @@ private function getWfsFeature()
'FEATUREID' => $featureId,
);
+ if ($exp_filter) {
+ $wfs_params['EXP_FILTER'] = $exp_filter;
+ unset($wfs_params['FEATUREID']);
+ }
+
$wfs_request = new \Lizmap\Request\WFSRequest(
$this->project,
$wfs_params,
lizmap::getServices()
);
- $this->featureData = null;
+ $featureData = null;
$wfs_response = $wfs_request->process();
// Check code
@@ -298,13 +360,17 @@ private function getWfsFeature()
return;
}
- $this->featureData = json_decode($wfs_response->getBodyAsString());
- if (empty($this->featureData)) {
- $this->featureData = null;
- } elseif (empty($this->featureData->features)) {
- $this->featureData = null;
+ $featureData = json_decode($wfs_response->getBodyAsString());
+ if (empty($featureData)) {
+ $featureData = null;
+ } elseif (empty($featureData->features)) {
+ $featureData = null;
}
+
+ return $featureData;
}
+
+ return null;
}
/**
@@ -438,7 +504,7 @@ public function modifyFeature()
}
// Check if data has been fetched via WFS for the feature
- $this->getWfsFeature();
+ $this->featureData = $this->getWfsFeature($this->layer, $this->featureId, $this->primaryKeys);
if (!$this->featureData) {
jMessage::add(jLocale::get('view~edition.message.error.feature.get'), 'featureNotFoundViaWfs');
@@ -495,7 +561,7 @@ public function editFeature()
}
// Check if data has been fetched via WFS for the feature
- $this->getWfsFeature();
+ $this->featureData = $this->getWfsFeature($this->layer, $this->featureId, $this->primaryKeys);
if (($this->featureId || $this->featureId === 0 || $this->featureId === '0') && !$this->featureData) {
jMessage::add(jLocale::get('view~edition.message.error.feature.get'), 'featureNotFoundViaWfs');
@@ -630,7 +696,7 @@ public function saveFeature()
}
// Get the data via a WFS request
- $this->getWfsFeature();
+ $this->featureData = $this->getWfsFeature($this->layer, $this->featureId, $this->primaryKeys);
// event to add custom field into the jForms form before setting data in it
$eventParams = array(
@@ -707,6 +773,15 @@ public function saveFeature()
return $rep;
}
+ // link mlayer to nlayer feature
+ if ($this->param('liz_pivot')) {
+ try {
+ $this->linkAddedFeature($this->param('liz_pivot'), $pkvals);
+ } catch (Exception $e) {
+ lizmap::getAppContext()->logMessage($e->getMessage(), 'lizmapadmin');
+ jMessage::add($e->getMessage(), 'linkAddedFeatureError');
+ }
+ }
// Redirect to the edition form or to the validate message
$next_action = $form->getData('liz_future_action');
jForms::destroy('view~edition', $this->featureId);
@@ -781,6 +856,138 @@ public function saveFeature()
return $rep;
}
+ /**
+ * Utilities used for link features related via an n to m relationship after an new "m" feature is created.
+ *
+ * @param string $linkedLayers information on feaures to link provided as "pivot-id:n-layer-id:n-layer-value"
+ * @param array $featureIds the primary keys of the new "m" feature
+ *
+ * @throws Exception
+ */
+ private function linkAddedFeature($linkedLayers, $featureIds)
+ {
+ // check pivot parameters
+ if (!$linkedLayers || count($featureIds) == 0) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.invalid.parameters'));
+ }
+
+ // lkPar[0]-> pivotId
+ // lkPar[1]-> $nLayerId
+ // lkPar[2]-> referenced field value of father layer in the pivot table
+ $lkPar = explode(':', $linkedLayers);
+
+ $pivotId = $lkPar[0];
+ $n_layerId = $lkPar[1];
+ $referencedFieldValue = $lkPar[2];
+
+ if (count($lkPar) !== 3 || !$pivotId || !$n_layerId || !$referencedFieldValue) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.invalid.parameters'));
+ }
+
+ // check pivot && n_Layer conf
+ /** @var qgisVectorLayer $pivotLayer The pivot vector layer instance */
+ $pivotLayer = $this->project->getLayer($pivotId);
+ if (!$pivotLayer) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.invalid.parameters'));
+ }
+
+ /** @var qgisVectorLayer $n_layer The n_layer vector layer instance */
+ $n_layer = $this->project->getLayer($n_layerId);
+ if (!$n_layer) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.invalid.parameters'));
+ }
+
+ $pivotLayerName = $pivotLayer->getName();
+ $n_layerName = $n_layer->getName();
+
+ // check if the layers is configured in attribute table
+ if (
+ !$this->project->hasAttributeLayers()
+ || !$this->project->hasAttributeLayersForLayer($pivotLayerName)
+ || !$this->project->isPivotLayer($pivotLayerName)
+ || !$this->project->hasAttributeLayersForLayer($n_layerName)
+
+ ) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.invalid.parameters'));
+ }
+
+ // get project relations and perform necessary checks
+ $relations = $this->project->getRelations();
+ if (
+ !is_array($relations)
+ || !array_key_exists('pivot', $relations)
+ || !array_key_exists($pivotId, $relations['pivot'])
+ || !array_key_exists($this->layerId, $relations['pivot'][$pivotId])
+ || !array_key_exists($n_layerId, $relations['pivot'][$pivotId])
+ || !array_key_exists($n_layerId, $relations)
+ || !array_key_exists($this->layerId, $relations)
+ ) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.association', array($this->layer->getTitle(), $n_layer->getTitle())));
+ }
+
+ $pivotConf = $relations['pivot'][$pivotId];
+ $m_layerId = $this->layerId;
+
+ $n_relation = null;
+ $m_relation = null;
+ foreach ($relations as $kRel => $relation) {
+ if ($kRel !== 'pivot') {
+ if ($kRel == $m_layerId || $kRel == $n_layerId) {
+ if (is_array($relation)) {
+ $filtered = array_filter($relation, function ($ar) use ($pivotId, $pivotConf, $kRel) {
+ return is_array($ar) && array_key_exists('referencingLayer', $ar) && array_key_exists('referencingField', $ar) && $ar['referencingLayer'] == $pivotId && $ar['referencingField'] == $pivotConf[$kRel];
+ });
+ if (count($filtered) == 1) {
+ if ($kRel == $m_layerId && !$m_relation) {
+ // mLayer -> current layer on editing
+ $m_relation = reset($filtered);
+ } elseif ($kRel == $n_layerId && !$n_relation) {
+ // nLayer -> layer related to nLayer via the pivot pivotId
+ $n_relation = reset($filtered);
+ }
+ }
+ }
+ }
+ }
+ }
+ // check information on relations and primary keys
+ if (
+ !$n_relation
+ || !$m_relation
+ || !array_key_exists($m_relation['referencedField'], $featureIds)
+ || !$featureIds[$m_relation['referencedField']]
+ ) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.association', array($this->layer->getTitle(), $n_layer->getTitle())));
+ }
+
+ $m_layerVal = $featureIds[$m_relation['referencedField']];
+ $m_layerField = $m_relation['referencingField'];
+ $n_layerVal = $referencedFieldValue;
+ $n_layerField = $n_relation['referencingField'];
+
+ // check if pivot is editable
+ $capabilities = $pivotLayer->getRealEditionCapabilities();
+ if ($capabilities->createFeature != 'True' || !$pivotLayer->canCurrentUserEdit()) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.association', array($this->layer->getTitle(), $n_layer->getTitle())));
+ }
+
+ $dbFieldsInfo = $pivotLayer->getDbFieldsInfo();
+ $dataFields = $dbFieldsInfo->dataFields;
+
+ // Check fields on pivot
+ if (!array_key_exists($n_layerField, $dataFields) || !array_key_exists($m_layerField, $dataFields)) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.association', array($this->layer->getTitle(), $n_layer->getTitle())));
+ }
+
+ try {
+ $pivotLayer->insertRelations($n_layerField, array($n_layerVal), $m_layerField, array($m_layerVal));
+ } catch (Exception $e) {
+ throw new Exception(jLocale::get('view~edition.linkaddedfeature.error.linkfeature.association', array($this->layer->getTitle(), $n_layer->getTitle())));
+ }
+
+ jMessage::add(jLocale::get('view~edition.linkaddedfeature.success.linkfeature', array($this->layer->getTitle(), $n_layer->getTitle())), 'linkAddedFeature');
+ }
+
/**
* Form close : destroy it and display a message.
*
@@ -811,9 +1018,25 @@ public function closeFeature()
$rep = $this->getResponse('htmlfragment');
$tpl = new jTpl();
+ $linkAddedFeatureError = jMessage::get('linkAddedFeatureError');
+ $linkAddedFeature = jMessage::get('linkAddedFeature');
+
+ // check for linked features messagges
+ $addedFeatureMessage = null;
+ $addedFeatureError = false;
+
+ if ($linkAddedFeatureError && is_array($linkAddedFeatureError)) {
+ $addedFeatureMessage = $linkAddedFeatureError[0];
+ $addedFeatureError = true;
+ } elseif ($linkAddedFeature && is_array($linkAddedFeature)) {
+ $addedFeatureMessage = $linkAddedFeature[0];
+ }
+
$closeFeatureMessage = jLocale::get('view~edition.form.data.saved');
$pkValsJson = $this->param('pkVals', '');
+ $tpl->assign('addedFeatureMessage', $addedFeatureMessage);
+ $tpl->assign('addedFeatureError', $addedFeatureError);
$tpl->assign('closeFeatureMessage', $closeFeatureMessage);
$tpl->assign('pkValsJson', $pkValsJson);
$content = $tpl->fetch('view~edition_close_feature_data');
@@ -826,6 +1049,7 @@ public function closeFeature()
/**
* Delete Feature (output as html fragment).
+ * If the Feature is linked with any pivot table (n to m relation) then the records on pivots are deleted too.
*
* @urlparam string $repository Lizmap Repository
* @urlparam string $project Name of the project
@@ -841,7 +1065,7 @@ public function deleteFeature()
}
// Check if data has been fetched via WFS for the feature
- $this->getWfsFeature();
+ $this->featureData = $this->getWfsFeature($this->layer, $this->featureId, $this->primaryKeys);
if (!$this->featureData) {
jMessage::add(jLocale::get('view~edition.message.error.feature.get'), 'featureNotFoundViaWfs');
@@ -901,37 +1125,150 @@ public function deleteFeature()
if ($event->allResponsesByKeyAreTrue('filesDeleted')) {
$deleteFiles = array();
}
+ $pivotFeatures = null;
+ if ($this->param('linkedRecords') && $this->param('linkedRecords') == 'ntom') {
+ // check all the pivot linked to the layer
+ $pivotFeatures = $this->getPivotFeatures();
+ }
try {
- $feature = $this->featureData->features[0];
- // delete record in the database
- $rs = $qgisForm->deleteFromDb($feature);
- if ($rs) {
- jMessage::add(jLocale::get('view~edition.message.success.delete'), 'success');
- $eventParams['deleteFiles'] = $deleteFiles;
- $eventParams['success'] = true;
-
- // delete associated files to the record
- foreach ($deleteFiles as $path) {
- if (file_exists($path)) {
- unlink($path);
+ // exec deletion in one transaction.
+ // This presumes that tables has the same datasource connection parameters.
+ // In order to use the same transaction, variable $cnx is passed along methods deleteFeature and deleteFromDb
+ $cnx = $this->layer->getDatasourceConnection();
+
+ $cnx->beginTransaction();
+
+ $relationsRemoved = true;
+ if ($pivotFeatures && count($pivotFeatures) > 0) {
+ foreach ($pivotFeatures as $pivotId => $pivotFeature) {
+ $currentPivotLayer = $pivotFeature['layer'];
+ foreach ($pivotFeature['features'] as $feature) {
+ // null value passed as loginFilterOverride -> is this necessary for pivot tables?
+ $rsp = $currentPivotLayer->deleteFeature($feature, null, $cnx);
+ if (!$rsp) {
+ $relationsRemoved = false;
+
+ break;
+ }
+ }
+ if (!$relationsRemoved) {
+ break;
}
}
+ }
+ if ($relationsRemoved) {
+ $feature = $this->featureData->features[0];
+ $rs = $qgisForm->deleteFromDb($feature, $cnx);
+
+ if ($rs) {
+ jMessage::add(jLocale::get('view~edition.message.success.delete'), 'success');
+ $eventParams['deleteFiles'] = $deleteFiles;
+ $eventParams['success'] = true;
+
+ // delete associated files to the record
+ foreach ($deleteFiles as $path) {
+ if (file_exists($path)) {
+ unlink($path);
+ }
+ }
+ $cnx->commit();
+ } else {
+ jMessage::add(jLocale::get('view~edition.message.error.delete'), 'error');
+ $eventParams['success'] = false;
+ $cnx->rollback();
+ }
} else {
jMessage::add(jLocale::get('view~edition.message.error.delete'), 'error');
$eventParams['success'] = false;
+ $cnx->rollback();
}
} catch (Exception $e) {
jLog::log('An error has been raised when saving form data edition to db:'.$e->getMessage(), 'lizmapadmin');
jLog::logEx($e, 'error');
jMessage::add(jLocale::get('view~edition.message.error.delete'), 'error');
$eventParams['success'] = false;
+ if (isset($cnx)) {
+ $cnx->rollback();
+ }
}
jEvent::notify('LizmapEditionPostDelete', $eventParams);
return $this->serviceAnswer();
}
+ /**
+ * Get Features via WFS from pivots connected to the current layer feature on editing.
+ *
+ * @return mixed $pivotFeature The array containing the features
+ */
+ protected function getPivotFeatures()
+ {
+ $pivotFeature = array();
+ $relations = $this->project->getRelations();
+
+ if (!is_array($relations)) {
+ return $pivotFeature;
+ }
+ if (!array_key_exists('pivot', $relations)) {
+ return $pivotFeature;
+ }
+ $pivots = $relations['pivot'];
+
+ foreach ($pivots as $pivotId => $pivotReference) {
+ if (array_key_exists($this->layerId, $pivotReference) && array_key_exists($this->layerId, $relations)) {
+ // check relation validity
+ foreach ($relations[$this->layerId] as $rel) {
+ if ($rel['referencingLayer'] == $pivotId && $rel['referencingField'] == $pivotReference[$this->layerId] && in_array($rel['referencedField'], $this->primaryKeys)) {
+ // avoid duplications
+ if (!array_key_exists($pivotId, $pivotFeature)) {
+ // check for edition capabilities
+ $editionLayerInfo = $this->getLayerEditionParameter($this->project, $pivotId, null, false);
+ if ($editionLayerInfo && is_array($editionLayerInfo)) {
+ if (array_key_exists($rel['referencedField'], $editionLayerInfo['dbFieldsInfo']->dataFields)) {
+ $currentLayer = $editionLayerInfo['layer'];
+ // checks on attribute table
+ if ($this->project->hasAttributeLayers() && $this->project->hasAttributeLayersForLayer($editionLayerInfo['layerName']) && $this->project->isPivotLayer($editionLayerInfo['layerName'])) {
+ // build filter for pivot table
+ $exp_filter = '"'.$rel['referencingField'].'" = \''.$this->featureId.'\'';
+ // get records via WFS service
+ $featureData = $this->getWfsFeature($currentLayer, $this->featureId, array(0 => $rel['referencingField']), $exp_filter);
+ if ($featureData) {
+ $eCapabilities = $currentLayer->getRealEditionCapabilities();
+ if ($eCapabilities->deleteFeature == 'True') {
+ foreach ($featureData->features as $feature) {
+ if (!$this->loginFilteredOverride) {
+ $is_editable = $currentLayer->isFeatureEditable($feature);
+ if (!$is_editable) {
+ continue;
+ }
+ }
+ $currentFeatureId = $feature->{$editionLayerInfo['primaryKeys'][0]};
+ if (!$currentFeatureId && $currentFeatureId !== 0 && $currentFeatureId !== '0') {
+ continue;
+ }
+ if (!array_key_exists($pivotId, $pivotFeature)) {
+ $pivotFeature[$pivotId] = array(
+ 'layer' => $currentLayer,
+ 'features' => array(),
+ );
+ }
+ $pivotFeature[$pivotId]['features'][] = $feature;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $pivotFeature;
+ }
+
/**
* Save a new feature, without redirecting to an HTML response.
*
@@ -1107,7 +1444,7 @@ public function editableFeatures()
return $rep;
}
- /** @var qgisVectorLayer $layer The QGIS vector layer instance */
+ /** @var null|qgisVectorLayer $layer The QGIS vector layer instance */
$layer = $lproj->getLayer($layerId);
if (!$layer) {
$rep->data['message'] = jLocale::get('view~edition.message.error.layer.editable');
diff --git a/lizmap/modules/lizmap/lib/Form/QgisForm.php b/lizmap/modules/lizmap/lib/Form/QgisForm.php
index 16f8454d79..c20232c9a8 100644
--- a/lizmap/modules/lizmap/lib/Form/QgisForm.php
+++ b/lizmap/modules/lizmap/lib/Form/QgisForm.php
@@ -1141,9 +1141,10 @@ protected function processWebDavUploadFile($form, $ref)
/**
* Delete the feature from the database.
*
- * @param mixed $feature
+ * @param mixed $feature
+ * @param null|\jDbConnection $cnx DBConnection, passed along QGISVectorLayer deleteFeature method
*/
- public function deleteFromDb($feature)
+ public function deleteFromDb($feature, $cnx = null)
{
if (!$this->dbFieldsInfo) {
throw new \Exception('Delete from database can\'t be done for the layer "'.$this->layer->getName().'"!');
@@ -1169,7 +1170,7 @@ public function deleteFromDb($feature)
if ($event->allResponsesByKeyAreFalse('cancelDelete') === false) {
return 0;
}
- $result = $this->layer->deleteFeature($feature, $loginFilteredLayers);
+ $result = $this->layer->deleteFeature($feature, $loginFilteredLayers, $cnx);
$this->appContext->eventNotify('LizmapEditionFeaturePostDelete', $eventParams);
return $result;
diff --git a/lizmap/modules/lizmap/lib/Project/Project.php b/lizmap/modules/lizmap/lib/Project/Project.php
index fa3412f31e..73d4ee2aef 100644
--- a/lizmap/modules/lizmap/lib/Project/Project.php
+++ b/lizmap/modules/lizmap/lib/Project/Project.php
@@ -842,6 +842,18 @@ public function hasAttributeLayersForLayer($layerName)
return property_exists($attributeLayers, $layerName);
}
+ public function isPivotLayer($layerName)
+ {
+ $attributeLayers = $this->cfg->getAttributeLayers();
+ if (property_exists($attributeLayers, $layerName)) {
+ $attributeLayer = $attributeLayers->{$layerName};
+
+ return property_exists($attributeLayer, 'pivot') && $attributeLayer->pivot == 'True';
+ }
+
+ return false;
+ }
+
public function hasFtsSearches()
{
// Virtual jdb profile corresponding to the layer database
diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
index 2859244d2a..6ea4e8c1df 100644
--- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
+++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
@@ -60,6 +60,9 @@ edition.revertgeom.success=Geometry has been reversed. You can now save the form
edition.confirm.launch.child.creation=You are about to create a new child object for the currently edited object. \nPlease check before that you have already saved your changes.\n\nDo you wish to continue?
edition.point.coord.crs.layer=Layer
edition.point.coord.crs.map=Map
+edition.link.pivot.add=The new record will be linked to the feature id %f of %l layer
+edition.confirm.pivot.unlink=Are you sure you want to unlink the selected feature from %l layer?
+edition.confirm.pivot.delete=Are you sure you want to delete the selected feature? \n\nThe related links with the following layers:\n %s \n\nwill be also deleted
externalsearch.search=Searching
externalsearch.notfound=No results
diff --git a/lizmap/modules/view/locales/en_US/edition.UTF-8.properties b/lizmap/modules/view/locales/en_US/edition.UTF-8.properties
index 8cd87045c8..92a42698b2 100644
--- a/lizmap/modules/view/locales/en_US/edition.UTF-8.properties
+++ b/lizmap/modules/view/locales/en_US/edition.UTF-8.properties
@@ -104,3 +104,7 @@ link.error.sql=An error occurred while adding linked data.
link.success=Selected features have been correctly linked.
unlink.success=The child feature has correctly been unlinked.
unlink.error.sql=An error occurred while removing link.
+
+linkaddedfeature.error.linkfeature.invalid.parameters=Required parameter for link the new feature are invalid
+linkaddedfeature.error.linkfeature.association=The association of the new "%s" feature to the "%s" layer has failed
+linkaddedfeature.success.linkfeature=The new feature of layer "%s" was successfully linked to the layer "%s"
\ No newline at end of file
diff --git a/lizmap/modules/view/templates/edition_close_feature_data.tpl b/lizmap/modules/view/templates/edition_close_feature_data.tpl
index 8ca528afe7..2bbafcdd3d 100644
--- a/lizmap/modules/view/templates/edition_close_feature_data.tpl
+++ b/lizmap/modules/view/templates/edition_close_feature_data.tpl
@@ -1,6 +1,11 @@
+{if $addedFeatureMessage}
+
+ {$addedFeatureMessage}
+
+{/if}