-
-
Notifications
You must be signed in to change notification settings - Fork 153
Migrate datatables from client to server side #5562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nboisteault
wants to merge
49
commits into
3liz:master
Choose a base branch
from
nboisteault:datatables-server-side
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
bd59d0d
Migrate datatables from client to server side
nboisteault 89892b0
DT: handle `selected` class for lines in `createdRow` callback
nboisteault 6e0c2cf
Attribute table: handle moveSelectedToTop w/ serverSide
nboisteault 7748131
Attribute table: handle filter w/ serverSide
nboisteault 513dc57
AttributeTable: add `data-layerid` in <table>s attributes to ease sel…
nboisteault 8a6e2a4
Handle 'selected' state for rows without querying server
nboisteault 70ea075
Get total number of records with WFS `RESULTTYPE=hits`
nboisteault 98fdcd2
Don't request WFS GetFeature at startup to get data as DT does
nboisteault 9d10f23
Handle children tables
nboisteault fce61a8
Return JSON features as they are cached in `lizMap.config`
nboisteault 36c43f9
Handle link/unlink in featureToolbars in attribute table
nboisteault 8c04889
Return `recordsFiltered` for Datatables
nboisteault 701c559
Redraw table if it already exists
nboisteault 8c58a19
Handle children tables follow up
nboisteault 9707926
Remove legacy code linked to `limitDataToBbox` parameter
nboisteault b765c7b
Attribute table: add a toggle button to filter features in the curren…
nboisteault 8626e72
e2e: test data filtered by extent in attribute table
nboisteault 7d11ae8
Attribute table: install Datatables v 2.2.2
nboisteault 12268e9
Attribute table: update code for Datatables v2.2.2
nboisteault 855d7fe
Add Datataables searchBuilder
nboisteault 7bef459
Add Datatables searchBuilder backend logic
nboisteault 5fcf493
Returns editableFeatures in datatables request
nboisteault a2b1849
e2e: migrate tests for DT2
nboisteault 8c42b9b
Refresh tables afetr edition
nboisteault 2b48053
e2e: update tests for DT2
nboisteault 87cb4d1
Some fixes to pass playwright tests
nboisteault 6a70e7a
e2e: update key_value_mapping test to wait for datatables to be loaded
nboisteault 612bf8e
Update DT tables when filters change
nboisteault 97f6494
Use `active` and not `btn-primary` class for filter and select button…
nboisteault 92a9767
Fix phpStan errors
nboisteault de405fb
Datatables: update i18n
nboisteault 6843865
Move selected to top button can now be toggled and only display selec…
nboisteault 99f6a49
DT: set `orderSequence` to have DT 1.x behaviour
nboisteault 4269d6c
e2e: fix some tests
nboisteault b466b0b
Use `active` with `btn-primary` class for filter and select buttons i…
rldhont 70927b8
DataTables request: send error and tests
rldhont 325a620
Tests requests DataTables: bbox filter
rldhont 1707a36
Fix and test request DataTables order
rldhont 5286243
Define \Lizmap\DataTables\DataTables with static method to convert DT…
rldhont 6ba83a0
DataTables: manage invalid criteria
rldhont fb150ab
Tests requests DataTables: Pages
rldhont 4de9bb7
Tests requests DataTables: Filter featureIds
rldhont 398e6b6
DataTables fix recordsTotal and recordsFiltered type to int
rldhont 72ffd1d
Tests requests DataTables: fix features type
rldhont b53e96e
Tests e2e Playwright attribute table: Fix bootstrap class
rldhont 19ff64e
Fix datatables requests to get number of features as JSON
rldhont 081217c
Fixing lizmap-feature-toolbar bbox to zoom to
rldhont 3fb1d0f
Fixing attribute table e2e tests
rldhont 11ab189
Fixing editable features e2e tests
rldhont File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
250 changes: 250 additions & 0 deletions
250
lizmap/modules/lizmap/controllers/datatables.classic.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Send data to datatables ajax requests. | ||
| * | ||
| * @author 3liz | ||
| * @copyright 2025 3liz | ||
| * | ||
| * @see https://3liz.com | ||
| * | ||
| * @license Mozilla Public License : http://www.mozilla.org/MPL/ | ||
| */ | ||
|
|
||
| use Lizmap\DataTables\DataTables; | ||
| use Lizmap\Project\UnknownLizmapProjectException; | ||
| use Lizmap\Request\Proxy; | ||
| use Lizmap\Request\WFSRequest; | ||
|
|
||
| class datatablesCtrl extends jController | ||
| { | ||
| /** | ||
| * Sets the error in the provided response object based on the given HTTP error code. | ||
| * | ||
| * @param jResponseJson $rep - the response object to which the error details will be assigned | ||
| * @param int $code - the HTTP error code | ||
| * @param string $errorMessage - the custom error message | ||
| * | ||
| * @return jResponseJson returns the updated response object containing the error details | ||
| */ | ||
| protected function setErrorResponse(jResponseJson $rep, int $code, string $errorMessage): jResponseJson | ||
| { | ||
| $rep->setHttpStatus($code, Proxy::getHttpStatusMsg($code)); | ||
| $rep->data = array( | ||
| 'code' => Proxy::getHttpStatusMsg($code), | ||
| 'status' => $code, | ||
| 'message' => $errorMessage, | ||
| ); | ||
|
|
||
| return $rep; | ||
| } | ||
|
|
||
| public function index() | ||
| { | ||
| /** @var jResponseJson $rep */ | ||
| $rep = $this->getResponse('json'); | ||
|
|
||
| // Lizmap parameters | ||
| $repository = $this->param('repository'); | ||
| $project = $this->param('project'); | ||
| $layerId = $this->param('layerId'); | ||
|
|
||
| if (!$repository || !$project || !$layerId) { | ||
| return $this->setErrorResponse($rep, 400, 'The parameters repository, project and layerId are mandatory.'); | ||
| } | ||
|
|
||
| // DataTables parameters | ||
| $DTStart = $this->param('start'); | ||
| $DTLength = $this->param('length'); | ||
| $DTOrder = $this->param('order'); | ||
| $DTColumns = $this->param('columns'); | ||
|
|
||
| // Check DataTables parameters | ||
| if (!isset($DTStart) || !isset($DTLength) || !isset($DTOrder) || !isset($DTColumns)) { | ||
| return $this->setErrorResponse($rep, 400, 'The DataTables parameters start, length'. | ||
| ', order and columns are mandatory.'); | ||
| } | ||
| if (!is_array($DTOrder) || count($DTOrder) == 0 || !array_key_exists(0, $DTOrder) | ||
| || !array_key_exists('column', $DTOrder[0]) || !array_key_exists('dir', $DTOrder[0])) { | ||
| return $this->setErrorResponse($rep, 400, 'The DataTables parameter order '.json_encode($DTOrder). | ||
| ' is not well formed.'); | ||
| } | ||
| if (!is_array($DTColumns) || count($DTColumns) == 0) { | ||
| return $this->setErrorResponse($rep, 400, 'The DataTables parameter columns '.json_encode($DTColumns). | ||
| ' is not well formed.'); | ||
| } | ||
|
|
||
| // Extract info for DataTables parameters | ||
| $DTOrderColumnIndex = $DTOrder[0]['column']; | ||
| $DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'DESC' : 'ASC'; | ||
| if (!array_key_exists($DTOrderColumnIndex, $DTColumns)) { | ||
| return $this->setErrorResponse($rep, 400, 'The DataTables parameters order and columns are not compatible.'); | ||
| } | ||
| if (!array_key_exists('data', $DTColumns[$DTOrderColumnIndex])) { | ||
| return $this->setErrorResponse($rep, 400, 'The DataTables parameter columns '.json_encode($DTColumns). | ||
| ' is not well formed.'); | ||
| } | ||
| $DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data']; | ||
|
|
||
| $DTSearchBuilder = ''; | ||
| if ($this->param('searchBuilder')) { | ||
| $DTSearchBuilder = $this->param('searchBuilder'); | ||
| } | ||
|
|
||
| $filteredFeatureIDs = array(); | ||
| if ($this->param('filteredfeatureids')) { | ||
| $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); | ||
| } | ||
| $expFilter = $this->param('exp_filter'); | ||
|
|
||
| // Filter by bounding box | ||
| $bbox = array(); | ||
| $srsName = $this->param('srsname'); | ||
| if ($this->param('bbox') && $srsName) { | ||
| $bbox = explode(',', $this->param('bbox')); | ||
| } | ||
|
|
||
| // Check if when the bbox is defined, it contains 4 number | ||
| if (count($bbox) > 0 && count($bbox) != 4) { | ||
| return $this->setErrorResponse($rep, 400, 'The bbox parameter must contain 4 numbers separated by a comma.'); | ||
| } | ||
|
|
||
| try { | ||
| $lproj = lizmap::getProject($repository.'~'.$project); | ||
| if (!$lproj) { | ||
| return $this->setErrorResponse($rep, 404, 'The lizmap project '.$repository.'~'.$project.' does not exist.'); | ||
| } | ||
| } catch (UnknownLizmapProjectException $e) { | ||
| return $this->setErrorResponse($rep, 404, 'The lizmap project '.$repository.'~'.$project.' does not exist.'); | ||
| } | ||
|
|
||
| /** @var null|qgisVectorLayer $layer */ | ||
| $layer = $lproj->getLayer($layerId); | ||
| if (!$layer) { | ||
| return $this->setErrorResponse($rep, 404, 'The layerId '.$layerId.' does not exist.'); | ||
| } | ||
| $typeName = $layer->getWfsTypeName(); | ||
|
|
||
| $jsonFeatures = array(); | ||
|
|
||
| $wfsParamsData = array( | ||
| 'SERVICE' => 'WFS', | ||
| 'VERSION' => '1.0.0', | ||
| 'REQUEST' => 'GetFeature', | ||
| 'TYPENAME' => $typeName, | ||
| 'OUTPUTFORMAT' => 'GeoJSON', | ||
| ); | ||
|
|
||
| // Get total number of features | ||
| $hits = 0; | ||
| $wfsParamsHits = array( | ||
| 'RESULTTYPE' => 'hits', | ||
| ); | ||
| // Get hits with WFS request | ||
| $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); | ||
| $wfsresponse = $wfsrequest->process(); | ||
|
|
||
| // Check response | ||
| if ($wfsresponse->getCode() >= 400) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get the total number of features failed, code: '.$wfsresponse->getCode()); | ||
| } | ||
| if (!str_contains(strtolower($wfsresponse->getMime()), 'application/vnd.geo+json')) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get the total number of features failed, mime-type: '.$wfsresponse->getMime()); | ||
| } | ||
|
|
||
| $hitsData = json_decode($wfsresponse->getBodyAsString()); | ||
| if (!property_exists($hitsData, 'numberOfFeatures')) { | ||
| return $this->setErrorResponse($rep, 400, 'The response of the request to get the total number of features is not well formed.'); | ||
| } | ||
|
|
||
| $hits = $hitsData->numberOfFeatures; | ||
| $recordsFiltered = $hits; | ||
| if (count($filteredFeatureIDs) > 0) { | ||
| $recordsFiltered = count($filteredFeatureIDs); | ||
| } | ||
|
|
||
| if (count($filteredFeatureIDs) > 0) { | ||
| $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; | ||
| } | ||
|
|
||
| // Handle search made by searchBuilder | ||
| if ($DTSearchBuilder) { | ||
| $expFilter = DataTables::convertSearchToExpression($DTSearchBuilder); | ||
| } | ||
|
|
||
| if ($expFilter) { | ||
| $wfsParamsData['EXP_FILTER'] = $expFilter; | ||
| } | ||
rldhont marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Handle filter by extent | ||
| if (count($bbox) == 4) { | ||
| // Add parameters to get features in the bounding box (paginated) | ||
| $bboxString = implode(',', $bbox); | ||
| $wfsParamsData['BBOX'] = $bboxString; | ||
| $wfsParamsData['SRSNAME'] = $srsName; | ||
| } | ||
|
|
||
| $wfsParamsPaginated = array( | ||
| 'MAXFEATURES' => $DTLength, | ||
| 'STARTINDEX' => $DTStart, | ||
| 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, | ||
| ); | ||
| // Get paginated features by a WFS resquest | ||
| $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsPaginated), lizmap::getServices()); | ||
| $wfsresponse = $wfsrequest->process(); | ||
|
|
||
| // Check response | ||
| if ($wfsresponse->getCode() >= 400) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get paginated features failed, code: '.$wfsresponse->getCode()); | ||
| } | ||
| if (!str_contains(strtolower($wfsresponse->getMime()), 'application/vnd.geo+json')) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get paginated features failed, mime-type: '.$wfsresponse->getMime()); | ||
| } | ||
|
|
||
| $featureData = $wfsresponse->getBodyAsString(); | ||
|
|
||
| // Get hits when data is filtered | ||
| if ($expFilter || count($bbox) == 4) { | ||
|
|
||
| $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); | ||
| $wfsresponse = $wfsrequest->process(); | ||
|
|
||
| // Check response | ||
| if ($wfsresponse->getCode() >= 400) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get the number of paginated features failed, code: '.$wfsresponse->getCode()); | ||
| } | ||
| if (!str_contains(strtolower($wfsresponse->getMime()), 'application/vnd.geo+json')) { | ||
| return $this->setErrorResponse($rep, 400, 'The request to get the number of paginated features failed, mime-type: '.$wfsresponse->getMime()); | ||
| } | ||
|
|
||
| $filterByExtentHitsData = json_decode($wfsresponse->getBodyAsString()); | ||
| if (!property_exists($hitsData, 'numberOfFeatures')) { | ||
| return $this->setErrorResponse($rep, 400, 'The response of the request to get the number of paginated features is not well formed.'); | ||
| } | ||
|
|
||
| $recordsFiltered = $filterByExtentHitsData->numberOfFeatures; | ||
| } | ||
|
|
||
| // Handle editable features | ||
| $editableFeaturesRep = $layer->editableFeatures(array_merge($wfsParamsData, $wfsParamsPaginated), false); | ||
| $editableFeaturesIds = array(); | ||
| foreach ($editableFeaturesRep['features'] as $feature) { | ||
| $editableFeaturesIds[] = (int) explode('.', $feature['id'])[1]; | ||
| } | ||
|
Comment on lines
+228
to
+233
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add to |
||
|
|
||
| unset($editableFeaturesRep['features']); | ||
| $editableFeaturesRep['featuresids'] = $editableFeaturesIds; | ||
|
|
||
| $returnedData = array( | ||
| 'draw' => (int) $this->param('draw'), | ||
| 'recordsTotal' => (int) $hits, | ||
| 'recordsFiltered' => (int) $recordsFiltered, | ||
| 'data' => json_decode($featureData), | ||
| 'editableFeatures' => $editableFeaturesRep, | ||
| ); | ||
|
|
||
| $rep->data = $returnedData; | ||
|
|
||
| return $rep; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add to
Lizmap\Request\WFSRequesta static const property.