Skip to content

Commit e5f9ca4

Browse files
committed
Unify workspace maps with maps endpoint
1 parent ed229dd commit e5f9ca4

14 files changed

Lines changed: 304 additions & 327 deletions

CHANGELOG.md

Lines changed: 27 additions & 26 deletions
Large diffs are not rendered by default.

doc/async-tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Asynchronous tasks are started by following requests:
99
- [PATCH Layer](rest.md#patch-layer)
1010
- tasks related to patched layer
1111
- tasks related to each map that points to patched layer
12-
- [POST Workspace Maps](rest.md#post-workspace-maps)
12+
- [POST Maps](rest.md#post-maps)
1313
- tasks related to newly published map
1414
- [PATCH Map](rest.md#patch-map)
1515
- tasks related to patched map

doc/client-proxy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ then response will change to
4949
```
5050

5151
Currently, value of X-Forwarded headers affects following URLs:
52-
* [GET Publications](rest.md#get-publications), [GET Layers](rest.md#get-layers), [GET Maps](rest.md#get-maps), and [GET Workspace Maps](rest.md#get-workspace-maps)
52+
* [GET Publications](rest.md#get-publications), [GET Layers](rest.md#get-layers), and [GET Maps](rest.md#get-maps)
5353
* `url` key
54-
* [POST Layers](rest.md#post-layers) and [POST Workspace Maps](rest.md#post-workspace-maps)
54+
* [POST Layers](rest.md#post-layers) and [POST Maps](rest.md#post-maps)
5555
* `url` key
5656
* [GET Layer](rest.md#get-layer) and [PATCH Layer](rest.md#patch-layer)
5757
* `url` key
@@ -72,7 +72,7 @@ Currently, value of X-Forwarded headers affects following URLs:
7272
* each `legends` key if its HTTP protocol and netloc corresponds with `url` or `protocol`.`url`
7373
* `style` key if its HTTP protocol and netloc corresponds with `url` or `protocol`.`url`
7474
* NOTE: If client proxy protocol, host, or URL path prefix was used in URLs in uploaded file, then such values are also replaced with values according to X-Forwarded header values. Default values are used for requests without X-Forwarded headers (protocol is the one from [LAYMAN_CLIENT_PUBLIC_URL](env-settings.md#layman_client_public_url), host is [LAYMAN_PROXY_SERVER_NAME](env-settings.md#layman_proxy_server_name), and path prefix is empty string).
75-
* [DELETE Layers](rest.md#delete-layers), [DELETE Workspace Maps](rest.md#delete-workspace-maps), [DELETE Layer](rest.md#delete-layer) and [DELETE Map](rest.md#delete-map)
75+
* [DELETE Layers](rest.md#delete-layers), [DELETE Maps](rest.md#delete-maps), [DELETE Layer](rest.md#delete-layer) and [DELETE Map](rest.md#delete-map)
7676
* `url` key
7777
* [OGC endpoints](endpoints.md)
7878
* Headers `X-Forwarded-For`, `X-Forwarded-Path`, `Forwarded` and `Host` are ignored

doc/data-storage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ When user [patches existing layer](rest.md#patch-layer), data is saved in the sa
4949
### Maps
5050
Information about [maps](models.md#map) includes JSON definition.
5151

52-
When user [publishes new map](rest.md#post-workspace-maps)
52+
When user [publishes new map](rest.md#post-maps)
5353
- UUID and name is saved to [Redis](#redis),
5454
- UUID, name, title, description and access rights are saved to [PostgreSQL](#postgresql),
5555
- JSON file is saved to [filesystem](#filesystem),

doc/publish-map.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ In QGIS, you need to implement following steps.
1212

1313
First, compose JSON valid against [map-composition schema](https://github.com/hslayers/map-compositions). For Layman, especially `describedBy`, `name`, `title`, `abstract`, `layers`, `projection`, and `extent attributes are important. Each layer must have `className` attribute equal to `HSLayers.Layer.WMS` or `WMS`.
1414

15-
Then save the file to Layman using [POST Workspace Maps](rest.md#post-workspace-maps) endpoint. Well-known [requests](https://requests.readthedocs.io/en/latest/) module can be used for sending HTTP requests. See especially
15+
Then save the file to Layman using [POST Maps](rest.md#post-maps) endpoint. Well-known [requests](https://requests.readthedocs.io/en/latest/) module can be used for sending HTTP requests. See especially
1616
- [More complicated POST requests](https://requests.readthedocs.io/en/latest/user/quickstart/#more-complicated-post-requests)
1717
- [POST a Multipart-Encoded File](https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file)
1818
- [POST Multiple Multipart-Encoded Files](https://requests.readthedocs.io/en/latest/user/advanced/#post-multiple-multipart-encoded-files)
1919

20-
In response of [POST Workspace Maps](rest.md#post-workspace-maps) you will obtain
20+
In response of [POST Maps](rest.md#post-maps) you will obtain
2121
- `name` of the map unique within all maps in used [workspace](models.md#workspace)
2222
- `url` of the map pointing to [GET Map](rest.md#get-map)
2323

@@ -27,7 +27,7 @@ In response of [POST Workspace Maps](rest.md#post-workspace-maps) you will obtai
2727
- update the map using [PATCH Map](rest.md#patch-map)
2828
- delete the map using [DELETE Map](rest.md#delete-map)
2929

30-
Also, you can obtain list of all maps using [GET Workspace Maps](rest.md#get-workspace-maps).
30+
Also, you can obtain list of all maps using [GET Maps](rest.md#get-maps).
3131

3232

3333
## Maps composed from vector files

doc/rest.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
|Layer Style|`/rest/layers/<uuid>/style`|[GET](#get-layer-style)| x | x | x |
1111
|Layer Chunk|`/rest/layers/<uuid>/chunk`|[GET](#get-layer-chunk)| [POST](#post-layer-chunk) | x | x |
1212
|Workspace Layer Metadata Comparison|`/rest/workspaces/<workspace_name>/layers/<layername>/metadata-comparison`|[GET](#get-workspace-layer-metadata-comparison) | x | x | x |
13-
|Maps|`/rest/maps`|[GET](#get-maps)| x | x | x |
13+
|Maps|`/rest/maps`|[GET](#get-maps)| [POST](#post-maps) | x | [DELETE](#delete-maps) |
1414
|[Map](models.md#map)|`/rest/maps/<uuid>`|[GET](#get-map)| x | [PATCH](#patch-map) | [DELETE](#delete-map) |
1515
|Map Thumbnail|`/rest/maps/<uuid>/thumbnail`|[GET](#get-map-thumbnail)| x | x | x |
1616
|Map File|`/rest/maps/<uuid>/file`|[GET](#get-map-file)| x | x | x |
17-
|Workspace Maps|`/rest/workspaces/<workspace_name>/maps`|[GET](#get-workspace-maps)| [POST](#post-workspace-maps) | x | [DELETE](#delete-workspace-maps) |
18-
|Workspace Map Metadata Comparison|`/rest/workspaces/<workspace_name>/layers/<layername>/metadata-comparison`|[GET](#get-workspace-map-metadata-comparison) | x | x | x |
17+
|Workspace Map Metadata Comparison|`/rest/workspaces/<workspace_name>/maps/<mapname>/metadata-comparison`|[GET](#get-workspace-map-metadata-comparison) | x | x | x |
1918
|Users|`/rest/users`|[GET](#get-users)| x | x | x |
2019
|User|`/rest/users/<username>`| x | x | x | [DELETE](#delete-user) |
2120
|Current [User](models.md#user)|`/rest/current-user`|[GET](#get-current-user)| x | [PATCH](#patch-current-user) | [DELETE](#delete-current-user) |
@@ -533,16 +532,12 @@ Get list of published maps (map compositions).
533532

534533
Have the same request parameters and response structure and headers as [GET Publications](#get-publications), except only maps are returned.
535534

536-
## Workspace Maps
537-
### URL
538-
`/rest/workspaces/<workspace_name>/maps`
539-
540-
### GET Workspace Maps
541-
Get list of published maps (map compositions).
542-
543-
Have the same request parameters and response structure and headers as [GET Maps](#get-maps).
535+
Query parameters:
536+
- *workspace*: String, optional
537+
- workspace identifier
538+
- if present, only maps from this workspace are returned
544539

545-
### POST Workspace Maps
540+
### POST Maps
546541
Publish new map composition. Accepts JSON valid against [map-composition schema](https://github.com/hslayers/map-compositions) version 2 or 3 used by [Hslayers-ng](https://github.com/hslayers/hslayers-ng). Exact version of schema is defined by `describedBy` key of JSON data file.
547542

548543
Processing chain consists of few steps:
@@ -563,6 +558,8 @@ Response to this request may be returned sooner than the processing chain is fin
563558
Content-Type: `multipart/form-data`
564559

565560
Body parameters:
561+
- **workspace**, string `^[a-z][a-z0-9]*(_[a-z0-9]+)*$`
562+
- workspace where the map will be published
566563
- *uuid*, string, e.g. `959c95fb-ab54-47a6-9694-402926b8fd29`
567564
- map primary key
568565
- used if specified, otherwise generated
@@ -603,11 +600,13 @@ JSON array of objects representing posted maps with following structure:
603600
- **uuid**: String. UUID of the map.
604601
- **url**: String. URL of the map. It points to [GET Map](#get-map).
605602

606-
### DELETE Workspace Maps
603+
### DELETE Maps
607604
Delete existing maps and all associated sources, including map-composition JSON file and map thumbnail for all maps in the workspace. The currently running [asynchronous tasks](async-tasks.md) of affected maps are aborted. Only maps on which user has [write access right](./security.md#access-to-multi-publication-endpoints) are deleted.
608605

609606
#### Request
610-
No action parameters.
607+
Query parameters:
608+
- **workspace**, string `^[a-z][a-z0-9]*(_[a-z0-9]+)*$`
609+
- workspace whose maps will be deleted
611610

612611
#### Response
613612
Content-Type: `application/json`
@@ -674,7 +673,7 @@ JSON object with following structure:
674673
- **native_bounding_box**: List of 4 floats. Bounding box coordinates [minx, miny, maxx, maxy] in native CRS.
675674

676675
### PATCH Map
677-
Update information about existing map. First, it deletes sources of the map, and then it publishes them again with new parameters. The processing chain is similar to [POST Workspace Maps](#post-workspace-maps), including [asynchronous tasks](async-tasks.md),
676+
Update information about existing map. First, it deletes sources of the map, and then it publishes them again with new parameters. The processing chain is similar to [POST Maps](#post-maps), including [asynchronous tasks](async-tasks.md),
678677

679678
Calling concurrent PATCH requests is not supported, as well as calling PATCH when [POST/PATCH async chain](async-tasks.md) is still running, is not allowed. In such cases, error is returned.
680679

@@ -683,7 +682,7 @@ Calling PATCH request when [WFS-T async chain](async-tasks.md) is still running
683682
#### Request
684683
Content-Type: `multipart/form-data`, `application/x-www-form-urlencoded`
685684

686-
Parameters have same meaning as in case of [POST Workspace Maps](#post-workspace-maps).
685+
Parameters have same meaning as in case of [POST Maps](#post-maps).
687686

688687
Body parameters:
689688
- *file*, JSON file
@@ -702,7 +701,7 @@ Body parameters:
702701
#### Response
703702
Content-Type: `application/json`
704703

705-
JSON object, same as in case of [POST Workspace Maps](#post-workspace-maps).
704+
JSON object, same as in case of [POST Maps](#post-maps).
706705

707706
### DELETE Map
708707
Delete existing map and all associated sources, including map-composition JSON file and map thumbnail. The currently running [asynchronous tasks](async-tasks.md) of affected map are aborted.
@@ -727,8 +726,8 @@ Get JSON file describing the map valid against [map-composition schema](https://
727726

728727
Notice that some JSON properties are automatically updated by layman, so file obtained by this endpoint may be slightly different from file that was uploaded. Expected changes:
729728
- **name** set to the map's name
730-
- **title** obtained from [POST Workspace Maps](#post-workspace-maps) or [PATCH Map](#patch-map) as `title`
731-
- **abstract** obtained from [POST Workspace Maps](#post-workspace-maps) or [PATCH Map](#patch-map) as `description`
729+
- **title** obtained from [POST Maps](#post-maps) or [PATCH Map](#patch-map) as `title`
730+
- **abstract** obtained from [POST Maps](#post-maps) or [PATCH Map](#patch-map) as `description`
732731
- **user** updated on the fly during this request:
733732
- **name** set to `<workspace_name>` in URL of this endpoint
734733
- **email** set to email of the owner, or empty string if not known

src/layman/map/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def get_map_patch_keys():
2727
MAP_REST_PATH_NAME = f"{PUBLICATION_TYPE_NAME}s"
2828

2929

30-
from .rest_workspace_maps import bp as workspace_maps_bp
3130
from .rest_map_thumbnail import bp as map_thumbnail_bp
3231
from .rest_map_file import bp as map_file_bp
3332
from .rest_workspace_map_metadata_comparison import bp as workspace_map_metadata_comparison_bp
@@ -41,7 +40,6 @@ def get_map_patch_keys():
4140
'name': PUBLICATION_TYPE_NAME,
4241
'rest_path_name': MAP_REST_PATH_NAME,
4342
'workspace_blueprints': [
44-
workspace_maps_bp,
4543
workspace_map_metadata_comparison_bp,
4644
],
4745
'blueprints': [

src/layman/map/micka/csw_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def provide_map(client):
5757
with app.app_context():
5858
workspace = TEST_WORKSPACE
5959
mapname = TEST_MAP
60-
rest_path = url_for('rest_workspace_maps.post', workspace=workspace)
60+
rest_path = url_for('rest_maps.post')
6161
file_paths = [
6262
'sample/layman.map/full.json',
6363
]
@@ -66,6 +66,7 @@ def provide_map(client):
6666
with ExitStack() as stack:
6767
files = [(stack.enter_context(open(fp, 'rb')), os.path.basename(fp)) for fp in file_paths]
6868
response = client.post(rest_path, data={
69+
'workspace': workspace,
6970
'file': files,
7071
'name': mapname,
7172
})

src/layman/map/rest_maps.py

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
from flask import Blueprint, g, request, current_app as app
1+
import json
2+
import io
23

3-
from layman import util as layman_util
4+
from flask import Blueprint, jsonify, request, g
5+
from flask import current_app as app
6+
from werkzeug.datastructures import FileStorage
7+
8+
from layman.http import LaymanError
9+
from layman import authn, util as layman_util, uuid
410
from layman.authn import authenticate, get_authn_username
5-
from layman.authz import authorize_publications_decorator
6-
from layman.common import rest as rest_common
7-
from . import MAP_TYPE, MAP_REST_PATH_NAME
11+
from layman.authz import authorize_publications_decorator, authorize
12+
from layman.common import redis as redis_util, rest as rest_common
13+
from layman.util import url_for
14+
from layman.uuid import register_publication_uuid_to_redis
15+
from . import util, MAP_TYPE, MAP_REST_PATH_NAME
16+
from .filesystem import input_file
817

918
bp = Blueprint('rest_maps', __name__)
1019

@@ -22,4 +31,129 @@ def get():
2231

2332
actor = get_authn_username()
2433
x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)
25-
return rest_common.get_publications(MAP_TYPE, actor, request_args=request.args, x_forwarded_items=x_forwarded_items)
34+
workspace = layman_util.get_workspace_from_request(request.args, required=False)
35+
if workspace:
36+
authorize(workspace, MAP_TYPE, None, request.method, actor)
37+
return rest_common.get_publications(
38+
MAP_TYPE,
39+
actor,
40+
request_args=request.args,
41+
workspace=workspace,
42+
x_forwarded_items=x_forwarded_items,
43+
)
44+
45+
46+
@bp.route(f"/{MAP_REST_PATH_NAME}", methods=['POST'])
47+
def post():
48+
app.logger.info(f"POST Maps, actor={g.user}")
49+
x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)
50+
51+
actor_name = authn.get_authn_username()
52+
workspace = layman_util.get_workspace_from_request(request.form, required=True)
53+
authorize(workspace, MAP_TYPE, None, request.method, actor_name)
54+
55+
# UUID
56+
input_uuid = request.form.get('uuid')
57+
input_uuid = input_uuid if input_uuid else None
58+
uuid.check_input_uuid(input_uuid)
59+
60+
# FILE
61+
if 'file' in request.files and not request.files['file'].filename == '':
62+
file = request.files["file"]
63+
else:
64+
raise LaymanError(1, {'parameter': 'file'})
65+
file_json = util.check_file(file, x_forwarded_items=x_forwarded_items)
66+
67+
# NAME
68+
unsafe_mapname = request.form.get('name', '')
69+
if len(unsafe_mapname) == 0:
70+
unsafe_mapname = input_file.get_unsafe_mapname(file_json)
71+
mapname = util.to_safe_map_name(unsafe_mapname)
72+
util.check_mapname(mapname)
73+
info = layman_util.get_publication_info(workspace, MAP_TYPE, mapname)
74+
if info:
75+
raise LaymanError(24, {'mapname': mapname})
76+
77+
# TITLE
78+
if len(request.form.get('title', '')) > 0:
79+
title = request.form['title']
80+
elif len(file_json.get('title', '')) > 0:
81+
title = file_json['title']
82+
else:
83+
title = mapname
84+
85+
# DESCRIPTION
86+
if len(request.form.get('description', '')) > 0:
87+
description = request.form['description']
88+
else:
89+
description = file_json.get('abstract', '')
90+
91+
redis_util.create_lock(workspace, MAP_TYPE, mapname, request.method)
92+
93+
try:
94+
map_result = {
95+
'name': mapname,
96+
}
97+
98+
kwargs = {
99+
'title': title,
100+
'description': description,
101+
'actor_name': actor_name,
102+
'x_forwarded_headers': x_forwarded_items.headers,
103+
}
104+
105+
rest_common.setup_post_access_rights(request.form, kwargs, actor_name)
106+
util.pre_publication_action_check(workspace,
107+
mapname,
108+
kwargs,
109+
)
110+
# register map uuid
111+
uuid_str = register_publication_uuid_to_redis(workspace, MAP_TYPE, mapname, input_uuid)
112+
kwargs['uuid'] = uuid_str
113+
114+
map_result.update({
115+
'uuid': uuid_str,
116+
'url': url_for('rest_map.get', uuid=uuid_str, x_forwarded_items=x_forwarded_items),
117+
})
118+
119+
file = FileStorage(
120+
io.BytesIO(json.dumps(file_json).encode()),
121+
file.filename
122+
)
123+
input_file.save_map_files(uuid_str, [file])
124+
125+
util.post_map(
126+
workspace,
127+
mapname,
128+
kwargs,
129+
'layman.map.filesystem.input_file'
130+
)
131+
except Exception as exception:
132+
try:
133+
if util.is_map_chain_ready(workspace, mapname):
134+
redis_util.unlock_publication(workspace, MAP_TYPE, mapname)
135+
finally:
136+
redis_util.unlock_publication(workspace, MAP_TYPE, mapname)
137+
raise exception
138+
139+
# app.logger.info('uploaded map '+mapname)
140+
return jsonify([map_result]), 200
141+
142+
143+
@bp.route(f"/{MAP_REST_PATH_NAME}", methods=['DELETE'])
144+
def delete():
145+
app.logger.info(f"DELETE Maps, actor={g.user}")
146+
147+
actor_name = authn.get_authn_username()
148+
workspace = layman_util.get_workspace_from_request(request.args, required=True)
149+
authorize(workspace, MAP_TYPE, None, request.method, actor_name)
150+
151+
x_forwarded_items = layman_util.get_x_forwarded_items(request.headers)
152+
infos = layman_util.delete_publications(
153+
workspace,
154+
MAP_TYPE,
155+
request.method,
156+
x_forwarded_items=x_forwarded_items,
157+
)
158+
159+
return infos, 200

0 commit comments

Comments
 (0)