Skip to content

Commit

Permalink
#7874 Add support for group attributes (#7875)
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz authored Dec 15, 2022
1 parent e8ee504 commit f5b346c
Show file tree
Hide file tree
Showing 14 changed files with 805 additions and 95 deletions.
16 changes: 16 additions & 0 deletions docs/developer-guide/mapstore-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ This is a list of things to check if you want to update from a previous version
- Optionally check also accessory files like `.eslinrc`, if you want to keep aligned with lint standards.
- Follow the instructions below, in order, from your version to the one you want to update to.

## Migration from 2022.02.02 to 2023.01.00

### Update database schema

This new version introduced the attributes for user groups. This requires an update to your database applying the scripts available [here](https://github.com/geosolutions-it/geostore/tree/master/doc/sql/migration). You have to apply the script `*-migration-from-v.1.5.0-to-v2.0.0` of your database. For instance on postgreSQL, you will have to execute the script `postgresql/postgresql-migration-from-v.1.5.0-to-v2.0.0`.

!!! note:
The script assumes you set the search path for your db schema. Usually in postgres it is `geostore`. So make you sure to set the proper search path before to execute the script in postgres. (e.g. `SET search_path TO geostore;` )

!!! note:
If you don't want to or you can not execute the migration script, you can set in `geostore-datasource-ovr.properities` the following property to make MapStore update the database for you

```properties
geostoreEntityManagerFactory.jpaPropertyMap[hibernate.hbm2ddl.auto]=update
```

## Migration from 2022.02.00 to 2022.02.01

### Package.json scripts migration
Expand Down
7 changes: 4 additions & 3 deletions web/client/actions/usergroups.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const USERGROUPMANAGER_SEARCH_TEXT_CHANGED = 'USERGROUPMANAGER_SEARCH_TEX
import API from '../api/GeoStoreDAO';

import { get } from 'lodash';
import assign from 'object-assign';

export function getUserGroupsLoading(text, start, limit) {
return {
Expand Down Expand Up @@ -204,15 +203,17 @@ export function createError(group, error) {
error
};
}

export function saveGroup(group, options = {}) {
return (dispatch) => {
let newGroup = assign({}, {...group});
let newGroup = {...group};
if (newGroup && newGroup.lastError) {
delete newGroup.lastError;
}
// update group
if (newGroup && newGroup.id) {
dispatch(savingGroup(newGroup));
return API.updateGroupMembers(newGroup, options).then((groupDetails) => {
return API.updateGroup(newGroup, options).then((groupDetails) => {
dispatch(savedGroup(groupDetails));
dispatch(getUserGroups());
}).catch((error) => {
Expand Down
20 changes: 17 additions & 3 deletions web/client/api/GeoStoreDAO.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,14 @@ const Api = {
getGroup: function(id, options = {}) {
const url = "usergroups/group/" + id;
return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) {
let groupLoaded = response.data.UserGroup;
let users = groupLoaded && groupLoaded.restUsers && groupLoaded.restUsers.User;
return {...groupLoaded, users: users && (Array.isArray(users) ? users : [users]) || []};
const groupLoaded = response.data.UserGroup;
const users = groupLoaded?.restUsers?.User;
const attributes = groupLoaded?.attributes;
return {
...groupLoaded,
users: users ? castArray(users) : undefined,
attributes: attributes ? castArray(attributes) : undefined
};
});
},
createGroup: function(group, options) {
Expand All @@ -436,6 +441,15 @@ const Api = {
return Api.updateGroupMembers({...group, id: groupId}, options);
}).then(() => groupId);
},
updateGroup: function(group, options) {
const id = group?.id;
const url = `usergroups/group/${id}`;
return axios.put(url, {UserGroup: {...group}}, this.addBaseUrl(parseOptions(options)))
.then(function() {
return Api.updateGroupMembers(group, options);
})
.then(() => id);
},
updateGroupMembers: function(group, options) {
// No GeoStore API to update group name and description. only update new users
if (group.newUsers) {
Expand Down
114 changes: 114 additions & 0 deletions web/client/api/__tests__/GeoStoreDAO-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,118 @@ describe('Test correctness of the GeoStore APIs', () => {
expect(data).toBe(1);
});
});
it('updateGroup with attributes', (done) => {
const sampleResponse = {
"UserGroup": {
"description": "description",
"enabled": true,
"groupName": "testGroup1",
"id": 10,
"attributes": [
{
"name": "notes",
"value": "test"
}
]
}
};

mockAxios.onPut().reply((data) => {
expect(data.baseURL).toEqual("/rest/geostore/");
expect(data.url).toEqual("usergroups/group/10");
expect(JSON.parse(data.data)).toEqual(sampleResponse);
return [200, "10"];
});
API.updateGroup({ id: 10, groupName: 'testGroup1', description: "description", enabled: true, attributes: [{name: "notes", value: "test"}]})
.then(data => {
expect(data).toEqual("10");
done();
})
.catch(e => {
done(e);
});
});
it('updateGroup with usergroups', (done) => {
const group = {
"status": "modified",
"description": "test",
"enabled": true,
"groupName": "testGroup1",
"id": 10,
"attributes": [
{
"name": "notes",
"value": "asdasd"
}
],
"restUsers": {
"User": {
"groupsNames": [
"everyone",
"testGroup1"
],
"id": 13,
"name": "user",
"role": "ADMIN"
}
},
"users": [
{
"groupsNames": [
"everyone",
"testGroup1"
],
"id": 13,
"name": "user",
"role": "ADMIN"
}
],
"newUsers": [
{
"enabled": true,
"groups": {
"group": {
"enabled": true,
"groupName": "everyone",
"id": 9
}
},
"id": 14,
"name": "test",
"role": "USER"
}
]
};
const checks = {};

mockAxios.onPut().reply((data) => {
expect(data.baseURL).toEqual("/rest/geostore/");
expect(data.url).toEqual("usergroups/group/10");
checks.put = true;
return [200, "10"];
});
mockAxios.onDelete().reply((data) => {
expect(data.baseURL).toEqual("/rest/geostore/");
expect(data.url).toEqual("/usergroups/group/13/10/");
checks.delete = true;
return [204, ""];
});
mockAxios.onPost().reply((data) => {
expect(data.baseURL).toEqual("/rest/geostore/");
expect(data.url).toEqual("/usergroups/group/14/10/");
checks.post = true;
return [204, "10"];
});
API.updateGroup(group)
.then(data => {
expect(data).toEqual("10");
expect(checks.put).toBe(true);
expect(checks.delete).toBe(true);
expect(checks.post).toBe(true);
done();
})
.catch(e => {
done(e);
});
});
});
111 changes: 111 additions & 0 deletions web/client/components/manager/users/AttributeControls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, {useState, useEffect} from 'react';
import axios from '../../../libs/ajax';
import { get, castArray, isArray, isNil } from 'lodash';
import Select from 'react-select';
import { FormControl } from 'react-bootstrap';
import { DateTimePicker } from 'react-widgets';

import moment from 'moment';
import momentLocalizer from 'react-widgets/lib/localizers/moment';
momentLocalizer(moment);


// new accessory selector for attribute editor

export const CONTROL_TYPES = { // note: from server can be "optional", anc can have a description, that doesn't seems to be used on the client
// types present in original portal
STRING: "string", // text line
TEXT: "text", // text area
// new type to implement our tools
SELECT: "select",
DATE: "date"

};

export const MultiValueSelect = ({onChange, options = [], value: currentValue, multiAttribute, separator = ",", isMulti, ...props}) => {
let wrapValue = [ ];
if (multiAttribute) {
wrapValue = castArray(currentValue);
} else {
wrapValue = currentValue.split?.(separator) ?? [];
}
const values = options.filter(({value: optValue}) => wrapValue.includes(optValue));
// multi and isMulti is for forward compatibility.
return (<Select
options={options}
isMulti={isMulti}
multi={isMulti}
value={isMulti ? values : values?.[0]}
onChange={onChange ? (rawValues = []) => {
const newValues = castArray(rawValues ?? []).map(({value: newVal}) => newVal);
if (multiAttribute) {
return onChange(!!newValues ? newValues : []);
}
const newValue = newValues.join(separator);
return onChange(!!newValue ? newValue : []);
} : undefined}
{...props}
/>);
};

export const RemoteSelect = ({source, ...props}) => {
const {url, path, valueAttribute = "value", labelAttribute = "label"} = source;
const [options, setOptions] = useState();
const loadOptions = () => axios.get(url).then(({data}) => {
return (path ? get(data, path) : data).map((entry = {}) => ({
value: entry?.[valueAttribute],
label: entry?.[labelAttribute]
}));
});
useEffect(() => {
loadOptions().then(setOptions);
}, []);
return <MultiValueSelect {...props} options={options}/>;
};

const MultiValueControl = ({name, value, onChange = () => {}, disabled, multiAttribute, separator, ...props}) => {

return (<FormControl
disabled={disabled}
type="input"
value={multiAttribute && isArray(value) ? value.join(separator) : value}
onChange={(e) => {
if (multiAttribute) {
const newValues = e.target.value && e.target.value.split(separator);
return onChange( newValues.length > 0 ? newValues : []);
}
// single value (empty strings are saved) TODO: opt empty string to remove attribute)
return onChange(e.target.value);
}}
name={name}
{...props} />);
};

export const DateControl = ({ name, value, onChange = () => { }, format, ...props}) => {
const handleChange = (date) => {
const newValue = moment(date).isValid() ? moment(date).format(format) : undefined;
onChange(newValue);
};
const date = !isNil(value) && !isNil(moment(value, format).toDate()) && moment(value, format).isValid() ? moment(value, format).toDate() : undefined;
return (<DateTimePicker
format={format}
time={false}
calendar
value={date}
onChange={handleChange}
name={name}
{...props} />);
};


export default {
[CONTROL_TYPES.SELECT]: ({name, value, options, source, controlAttributes = {}, onChange = () => {}}) =>
source
? <RemoteSelect name={name} value={value} onChange={onChange} {...controlAttributes} source={source} />
: <MultiValueSelect name={name} value={value} onChange={onChange} {...controlAttributes} options={options ?? [{ value: value, label: value }]}/>,
[CONTROL_TYPES.TEXT]: ({name, value, onChange = () => {}, disabled, controlAttributes = {} }) => <MultiValueControl componentClass="textarea" disabled={disabled} onChange={onChange} value={value} name={name} {...controlAttributes} />,
[CONTROL_TYPES.STRING]: ({name, value, onChange = () => {}, disabled, controlAttributes = {} }) => <MultiValueControl type="text" disabled={disabled} onChange={onChange} value={value} name={name} {...controlAttributes} />,
[CONTROL_TYPES.DATE]: ({ name, value, onChange = () => { }, disabled, controlAttributes = {} }) => <DateControl disabled={disabled} onChange={onChange} value={value} name={name} {...controlAttributes} />
};


Loading

0 comments on commit f5b346c

Please sign in to comment.