Skip to content

Commit b5a6cab

Browse files
authored
#292 - Error loading locations on OpenMRS 2.7 (#293)
1 parent ada6a78 commit b5a6cab

File tree

7 files changed

+286
-40
lines changed

7 files changed

+286
-40
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ See the [documentation on Initializer's logging properties](readme/rtprops.md#lo
208208

209209
#### Version 2.9.0
210210
* Fix for InitializerSerializer to ensure compatibility with OpenMRS version 2.7.0+
211+
* Fix for processing attributes to ensure compatibility with OpenMRS version 2.7.0+
211212

212213
#### Version 2.8.0
213214
* Ampath forms translation files will now generate checksums.

api-2.7/src/test/java/org/openmrs/module/initializer/api/loaders/GlobalPropertiesLoaderIntegration_2_7_Test.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import org.springframework.beans.factory.annotation.Autowired;
1919

2020
/**
21-
* This test is intended to be a copy of the GlobalPropertiesLoaderIntegrationTest from the api module,
22-
* but included within the api-2.7 module to test that global properties loading from xml works successfully
23-
* in an OpenMRS 2.7 environment
21+
* This test is intended to be a copy of the GlobalPropertiesLoaderIntegrationTest from the api
22+
* module, but included within the api-2.7 module to test that global properties loading from xml
23+
* works successfully in an OpenMRS 2.7 environment
2424
*/
2525
public class GlobalPropertiesLoaderIntegration_2_7_Test extends DomainBaseModuleContextSensitive_2_7_Test {
2626

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.initializer.api.loaders;
11+
12+
import org.junit.Assert;
13+
import org.junit.Before;
14+
import org.junit.Test;
15+
import org.openmrs.Location;
16+
import org.openmrs.LocationAttribute;
17+
import org.openmrs.LocationTag;
18+
import org.openmrs.api.LocationService;
19+
import org.openmrs.api.context.Context;
20+
import org.openmrs.customdatatype.datatype.DateDatatype;
21+
import org.openmrs.module.initializer.DomainBaseModuleContextSensitive_2_7_Test;
22+
import org.openmrs.module.initializer.api.loc.LocationsLoader;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.beans.factory.annotation.Qualifier;
25+
26+
import java.util.Collection;
27+
import java.util.Date;
28+
import java.util.Locale;
29+
import java.util.Set;
30+
31+
import static org.hamcrest.CoreMatchers.is;
32+
import static org.hamcrest.CoreMatchers.notNullValue;
33+
import static org.hamcrest.CoreMatchers.nullValue;
34+
35+
/**
36+
* This is a copy of the LocationsLoaderIntegration test in order to test the same test against
37+
* OpenMRS 2.7
38+
*/
39+
public class LocationsLoader_2_7_IntegrationTest extends DomainBaseModuleContextSensitive_2_7_Test {
40+
41+
@Autowired
42+
@Qualifier("locationService")
43+
private LocationService ls;
44+
45+
@Autowired
46+
private LocationsLoader loader;
47+
48+
@Autowired
49+
private DateDatatype dateDatatype;
50+
51+
private Locale localeEn = Locale.ENGLISH;
52+
53+
private Locale localeKm = new Locale("km", "KH");
54+
55+
@Before
56+
public void setup() throws Exception {
57+
executeDataSet("testdata/test-metadata.xml");
58+
}
59+
60+
@Test
61+
public void load_shouldLoadAccordingToCsvFiles() {
62+
// Pre-load verif
63+
{
64+
Location loc = ls.getLocation(4089);
65+
Assert.assertEquals("a03e395c-b881-49b7-b6fc-983f6bddc7fc", loc.getUuid());
66+
Assert.assertEquals("Acme Clinic", loc.getName());
67+
Assert.assertThat(loc.getDescription(), nullValue());
68+
Assert.assertThat(loc.getParentLocation(), nullValue());
69+
Assert.assertThat(loc.getTags().size(), is(0));
70+
71+
Collection<LocationAttribute> attributes = loc.getActiveAttributes();
72+
Assert.assertThat(attributes.size(), is(1));
73+
Assert.assertEquals("2016-04-14",
74+
dateDatatype.serialize((Date) ((LocationAttribute) attributes.toArray()[0]).getValue()));
75+
}
76+
{
77+
Location loc = ls.getLocation(4090);
78+
Assert.assertEquals("cbaaaab4-d960-4ae9-9b6a-8983fbd947b6", loc.getUuid());
79+
Assert.assertEquals("Legacy Location", loc.getName());
80+
Assert.assertEquals("Legacy location that must be retired", loc.getDescription());
81+
Assert.assertThat(loc.getRetired(), is(false));
82+
}
83+
84+
// Replay
85+
loader.load();
86+
87+
// Verify fetch by name
88+
{
89+
Location loc = ls.getLocation("LOCATION_NO_UUID");
90+
Assert.assertEquals("Main Street", loc.getAddress1());
91+
Assert.assertEquals("fdddc31a-3930-11ea-9712-a73c3c19744f", loc.getUuid());
92+
}
93+
// Verify creation with dynamic tag creation
94+
{
95+
Location loc = ls.getLocation("The Lake Clinic-Cambodia");
96+
Assert.assertEquals("Paradise Street", loc.getAddress1());
97+
Assert.assertEquals("Siem Reap", loc.getCountyDistrict());
98+
Assert.assertEquals("Siem Reap", loc.getStateProvince());
99+
Assert.assertEquals("Cambodia", loc.getCountry());
100+
101+
Set<LocationTag> tags = loc.getTags();
102+
Assert.assertThat(tags, notNullValue());
103+
Assert.assertThat(tags.size(), is(2));
104+
Assert.assertThat(tags.contains(ls.getLocationTagByName("Login Location")), is(true));
105+
Assert.assertThat(tags.contains(ls.getLocationTagByName("Another Location Tag")), is(true));
106+
107+
Collection<LocationAttribute> attributes = loc.getActiveAttributes();
108+
Assert.assertThat(attributes.size(), is(2));
109+
Assert.assertEquals("CODE-TLC-123", ((LocationAttribute) attributes.toArray()[0]).getValue());
110+
Assert.assertEquals("2017-05-15",
111+
dateDatatype.serialize((Date) ((LocationAttribute) attributes.toArray()[1]).getValue()));
112+
}
113+
// Verify creation with Tag| headers
114+
{
115+
Location loc = ls.getLocation("OPD Room");
116+
Assert.assertEquals(ls.getLocation("The Lake Clinic-Cambodia"), loc.getParentLocation());
117+
118+
Set<LocationTag> tags = loc.getTags();
119+
Assert.assertThat(tags, notNullValue());
120+
Assert.assertThat(tags.size(), is(1));
121+
Assert.assertThat(tags.contains(ls.getLocationTagByName("Facility Location")), is(true));
122+
}
123+
// Verify that the provided UUID is correctly assigned
124+
{
125+
Location loc = ls.getLocationByUuid("1cb58794-3c49-11ea-b3eb-f7801304f314");
126+
Assert.assertNotNull(loc);
127+
Assert.assertEquals("New Location", loc.getName());
128+
}
129+
// Verify edit
130+
{
131+
Location loc = ls.getLocationByUuid("a03e395c-b881-49b7-b6fc-983f6bddc7fc");
132+
Assert.assertEquals("Acme Clinic", loc.getName());
133+
Assert.assertEquals("This now becomes a child of TLC", loc.getDescription());
134+
Assert.assertEquals(ls.getLocation("The Lake Clinic-Cambodia"), loc.getParentLocation());
135+
136+
Set<LocationTag> tags = loc.getTags();
137+
Assert.assertThat(tags, notNullValue());
138+
Assert.assertThat(tags.size(), is(1));
139+
Assert.assertThat(tags.contains(ls.getLocationTagByName("Login Location")), is(true));
140+
141+
Collection<LocationAttribute> attributes = loc.getActiveAttributes();
142+
Assert.assertThat(attributes.size(), is(1));
143+
Assert.assertEquals("2019-03-13",
144+
dateDatatype.serialize((Date) ((LocationAttribute) attributes.toArray()[0]).getValue()));
145+
146+
}
147+
// Verify retire
148+
{
149+
Location loc = ls.getLocationByUuid("cbaaaab4-d960-4ae9-9b6a-8983fbd947b6");
150+
Assert.assertThat(loc.getRetired(), is(true));
151+
}
152+
// Verify that location with an invalid parent isn't created
153+
{
154+
Location loc = ls.getLocationByUuid("2b9824a3-92f0-4966-8f34-1b105624b267");
155+
Assert.assertNull(loc);
156+
}
157+
158+
// verify Locations domain i18n on entries with display:xy fields
159+
{
160+
Assert.assertEquals("Acme Clinic (translated)", Context.getMessageSourceService()
161+
.getMessage("ui.i18n.Location.name.a03e395c-b881-49b7-b6fc-983f6bddc7fc", null, localeEn));
162+
Assert.assertEquals("គ្លីនិកអាមី", Context.getMessageSourceService()
163+
.getMessage("ui.i18n.Location.name.a03e395c-b881-49b7-b6fc-983f6bddc7fc", null, localeKm));
164+
Assert.assertEquals("Acme Clinic (translated)", Context.getMessageSourceService()
165+
.getMessage("org.openmrs.Location.a03e395c-b881-49b7-b6fc-983f6bddc7fc", null, localeEn));
166+
Assert.assertEquals("គ្លីនិកអាមី", Context.getMessageSourceService()
167+
.getMessage("org.openmrs.Location.a03e395c-b881-49b7-b6fc-983f6bddc7fc", null, localeKm));
168+
}
169+
// verify no Locations domain i18n on entries without display:xy fields and entries without pre-filled uuids
170+
{
171+
Assert.assertNotNull(ls.getLocationByUuid("1cb58794-3c49-11ea-b3eb-f7801304f314"));
172+
Assert.assertEquals("ui.i18n.Location.name.1cb58794-3c49-11ea-b3eb-f7801304f314",
173+
Context.getMessageSourceService().getMessage("ui.i18n.Location.name.1cb58794-3c49-11ea-b3eb-f7801304f314",
174+
null, localeEn));
175+
Assert.assertEquals("ui.i18n.Location.name.1cb58794-3c49-11ea-b3eb-f7801304f314",
176+
Context.getMessageSourceService().getMessage("ui.i18n.Location.name.1cb58794-3c49-11ea-b3eb-f7801304f314",
177+
null, localeKm));
178+
179+
Assert.assertEquals("org.openmrs.Location.1cb58794-3c49-11ea-b3eb-f7801304f314",
180+
Context.getMessageSourceService().getMessage("org.openmrs.Location.1cb58794-3c49-11ea-b3eb-f7801304f314",
181+
null, localeEn));
182+
Assert.assertEquals("org.openmrs.Location.1cb58794-3c49-11ea-b3eb-f7801304f314",
183+
Context.getMessageSourceService().getMessage("org.openmrs.Location.1cb58794-3c49-11ea-b3eb-f7801304f314",
184+
null, localeKm));
185+
186+
Location loc = ls.getLocation("The Lake Clinic-Cambodia");
187+
Assert.assertNotNull(loc);
188+
189+
String uuid = loc.getUuid();
190+
Assert.assertEquals("ui.i18n.Location.name." + uuid,
191+
Context.getMessageSourceService().getMessage("ui.i18n.Location.name." + uuid, null, localeEn));
192+
Assert.assertEquals("ui.i18n.Location.name." + uuid,
193+
Context.getMessageSourceService().getMessage("ui.i18n.Location.name." + uuid, null, localeKm));
194+
Assert.assertEquals("org.openmrs.Location." + uuid,
195+
Context.getMessageSourceService().getMessage("org.openmrs.Location." + uuid, null, localeEn));
196+
Assert.assertEquals("org.openmrs.Location." + uuid,
197+
Context.getMessageSourceService().getMessage("org.openmrs.Location." + uuid, null, localeKm));
198+
}
199+
}
200+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Uuid,Void/Retire,Name,Description,Parent,Tags,Tag|Facility Location,Attribute|9eca4f4e-707f-4bb8-8289-2f9b6e93803c,Attribute|Last Audit Date,Address 1,Address 2,Address 3,Address 4,Address 5,Address 6,City/Village,County/District,State/Province,Postal Code,Country,display:en,display:km_KH,_order:1000
2+
,,The Lake Clinic-Cambodia,,,Login Location; Another Location Tag,,CODE-TLC-123,2017-05-15,Paradise Street,,,,,,,Siem Reap,Siem Reap,,Cambodia,The Lake Clinic-Cambodia (translated),គ្លីនីកគ្លីនិក - ប្រទេសកម្ពុជា,
3+
,,OPD Room,,The Lake Clinic-Cambodia,,TRUE,,,,,,,,,,,,,,,,
4+
a03e395c-b881-49b7-b6fc-983f6bddc7fc,,Acme Clinic,This now becomes a child of TLC,The Lake Clinic-Cambodia,Login Location,,,2019-03-13,,,,,,,,,,,,Acme Clinic (translated),គ្លីនិកអាមី,
5+
cbaaaab4-d960-4ae9-9b6a-8983fbd947b6,TRUE,Legacy Location,Legacy location that must be retired,,,,,,,,,,,,,,,,,,,
6+
,,LOCATION_NO_UUID,,,,,,,Main Street,,,,,,,Siem Reap,Siem Reap,,Cambodia,,,
7+
1cb58794-3c49-11ea-b3eb-f7801304f314,,New Location,,,,,,,,,,,,,,,,,,,,
8+
2b9824a3-92f0-4966-8f34-1b105624b267,,Invalid parent location,This location shouldn not be loaded as it has an invalid parent,Invalid parent,,,,,,,,,,,,,,,,,,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Uuid,Void/Retire,Name,Description
2+
,,Sparse,
3+
b03e395c-b881-49b7-b6fc-983f6befc7fc,,Filled in,A tag with all its fields
4+
a2327745-2970-4752-ac8a-dd0ba131f40e,TRUE,Facility Location,
5+
a1417745-1170-5752-fc8a-dd0ba131f40e,,Supply Room,Don't call it a shed

api/src/main/java/org/openmrs/module/initializer/api/BaseAttributeLineProcessor.java

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package org.openmrs.module.initializer.api;
22

3-
import java.util.AbstractMap;
4-
import java.util.AbstractMap.SimpleEntry;
5-
import java.util.Arrays;
6-
import java.util.Collection;
7-
import java.util.function.Consumer;
8-
93
import org.apache.commons.lang3.StringUtils;
104
import org.openmrs.BaseOpenmrsObject;
5+
import org.openmrs.api.context.Context;
116
import org.openmrs.attribute.Attribute;
127
import org.openmrs.attribute.BaseAttribute;
138
import org.openmrs.attribute.BaseAttributeType;
149
import org.openmrs.customdatatype.CustomDatatype;
1510
import org.openmrs.customdatatype.CustomDatatypeUtil;
1611
import org.openmrs.customdatatype.Customizable;
12+
import org.openmrs.module.initializer.InitializerConstants;
13+
14+
import java.util.Date;
15+
import java.util.LinkedHashMap;
16+
import java.util.Map;
1717

1818
/**
1919
* Base class to any <code>AttributeLineProcessor</code> that processes all attributes of a domain
@@ -28,32 +28,64 @@ public T fill(T instance, CsvLine line) throws IllegalArgumentException {
2828

2929
Customizable<A> attributable = (Customizable<A>) instance;
3030

31-
Consumer<? super SimpleEntry<String, String>> processAttributeData = attData -> {
32-
33-
AT attType = getAttributeType(attData.getKey());
34-
if (attType == null) {
35-
throw new IllegalArgumentException("An attribute value is specified ('" + attData.getValue()
36-
+ "') for an attribute type that cannot be resolved by the following identifier: '"
37-
+ attData.getKey() + "'");
31+
// First, retrieve all attributes that are defined in the CSV and the values for the given row
32+
Map<AT, Object> rowValues = new LinkedHashMap<>();
33+
for (String header : line.getHeaderLine()) {
34+
if (header.toLowerCase().startsWith(HEADER_ATTRIBUTE_PREFIX)) {
35+
String attributeTypeIdentifier = StringUtils.removeStartIgnoreCase(header, HEADER_ATTRIBUTE_PREFIX);
36+
AT attributeType = getAttributeType(attributeTypeIdentifier);
37+
Object attributeValue = null;
38+
String attributeValueStr = line.getString(header, "");
39+
if (attributeType == null) {
40+
throw new IllegalArgumentException("An attribute value is specified ('" + attributeValueStr
41+
+ "') for an attribute type that cannot be resolved by the following identifier: '"
42+
+ attributeTypeIdentifier + "'");
43+
}
44+
if (StringUtils.isNotBlank(attributeValueStr)) {
45+
String dtClass = attributeType.getDatatypeClassname();
46+
String dtConfig = attributeType.getDatatypeConfig();
47+
CustomDatatype<?> datatype = CustomDatatypeUtil.getDatatype(dtClass, dtConfig);
48+
attributeValue = datatype.fromReferenceString(attributeValueStr);
49+
}
50+
rowValues.put(attributeType, attributeValue);
3851
}
39-
40-
attributable.getAttributes().removeIf(att -> att.getAttributeType().equals(attType));
41-
42-
CustomDatatype<?> datatype = CustomDatatypeUtil.getDatatype(attType.getDatatypeClassname(),
43-
attType.getDatatypeConfig());
44-
Object value = datatype.fromReferenceString(attData.getValue());
45-
46-
A attribute = newAttribute();
47-
attribute.setAttributeType(attType);
48-
attribute.setValue(value);
49-
50-
attributable.addAttribute(attribute);
51-
};
52+
}
5253

53-
// process the attribute value from each attribute header
54-
Arrays.stream(line.getHeaderLine()).filter(h -> StringUtils.startsWithIgnoreCase(h, HEADER_ATTRIBUTE_PREFIX)).map(
55-
h -> new AbstractMap.SimpleEntry<>(StringUtils.removeStartIgnoreCase(h, HEADER_ATTRIBUTE_PREFIX), line.get(h)))
56-
.filter(attData -> StringUtils.isNotBlank(attData.getValue())).forEach(processAttributeData);
54+
// Next, iterate over the existing attributes on the instance, and void and recreate if any have changed
55+
for (A existingAttribute : attributable.getActiveAttributes()) {
56+
AT type = existingAttribute.getAttributeType();
57+
Object existingValue = existingAttribute.getValue();
58+
// If the CSV does not define a column for an existing attribute type, do not modify it on the instance
59+
if (rowValues.containsKey(type)) {
60+
Object newValue = rowValues.remove(type);
61+
// Only process a change if the new value is different from the existing value
62+
if (!existingValue.equals(newValue)) {
63+
// Void existing attribute
64+
existingAttribute.setVoided(true);
65+
existingAttribute.setDateVoided(new Date());
66+
existingAttribute.setVoidedBy(Context.getAuthenticatedUser());
67+
existingAttribute.setVoidReason(InitializerConstants.DEFAULT_VOID_REASON);
68+
// Add new attribute, if there is a value defined for it
69+
if (newValue != null) {
70+
A newAttribute = newAttribute();
71+
newAttribute.setAttributeType(type);
72+
newAttribute.setValue(newValue);
73+
attributable.addAttribute(newAttribute);
74+
}
75+
}
76+
}
77+
}
78+
79+
// Finally, add any remaining attributes that did not match any existing types on the instance
80+
for (AT type : rowValues.keySet()) {
81+
Object newValue = rowValues.get(type);
82+
if (newValue != null) {
83+
A newAttribute = newAttribute();
84+
newAttribute.setAttributeType(type);
85+
newAttribute.setValue(newValue);
86+
attributable.addAttribute(newAttribute);
87+
}
88+
}
5789

5890
return instance;
5991
}

api/src/test/resources/testdata/test-metadata.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
<location_attribute_type LOCATION_ATTRIBUTE_TYPE_ID="1089" NAME="Location Code" DATATYPE="org.openmrs.customdatatype.datatype.FreeTextDatatype" MIN_OCCURS="0" CREATOR="1" DATE_CREATED="2019-12-11 09:31:48.0" RETIRED="false" UUID="9eca4f4e-707f-4bb8-8289-2f9b6e93803c"/>
2727
<location_attribute_type LOCATION_ATTRIBUTE_TYPE_ID="1090" NAME="Last Audit Date" DATATYPE="org.openmrs.customdatatype.datatype.DateDatatype" CREATOR="1" DATE_CREATED="2005-01-01 00:00:00.0" MIN_OCCURS="0" RETIRED="false" UUID="9516cc50-6f9f-11e0-8414-001e378eb67e"/>
28-
28+
2929
<location_attribute location_attribute_id="1089" location_id="4089" attribute_type_id="1090" value_reference="2016-04-14" creator="1" date_created="2005-01-01 00:00:00.0" voided="false" uuid="3a2bdb18-6faa-11e0-8414-001e378eb67e"/>
3030

3131
<location_tag_map location_id="4092" location_tag_id="4089"/>
@@ -41,18 +41,18 @@
4141
<privilege privilege="Add Apples" description="Able to add apples." uuid="4604e928-96bf-4e2c-be08-cbbc40dd000c"/>
4242
<privilege privilege="Add Reports" description="Able to add analytic reports." uuid="cf68a296-2700-102b-80cb-0017a47871b2"/>
4343
<privilege privilege="Add Hens" description="Able to add poultry." uuid="36404041-c255-4c5b-9b47-0d757d2afa95"/>
44-
44+
4545
<role ROLE="test-entity-role" UUID="3f8f0ab7-c240-4b68-8951-bb7020be01f6"/>
46-
46+
4747
<patient_identifier_type patient_identifier_type_id="1" name="OpenMRS ID" description="OpenMRS Identification Number" creator="1" date_created="2020-03-1 15:59:20.0" required="false" format="" format_description="" validator="" retired="false" uuid="b0d10dc0-d8ce-11e3-9c1a-0jhg89c9a66" />
4848
<patient_identifier_type patient_identifier_type_id="2" name="Legacy ID" description="Legacy Identification Number" creator="1" date_created="2020-03-1 15:59:20.0" required="false" format="" format_description="" validator="" retired="false" uuid="8aaacb14-5ac0-4226-9a6e-fee8e9d92aeb" />
49-
50-
<idgen_identifier_source id="1" name="OpenMRS ID Generator" identifier_type="1" creator="1" date_created="2009-11-30 11:28:30.489" retired="false" uuid="0d47284f-9e9b-4a81-a88b-8bb42bc0a901" />
49+
50+
<idgen_identifier_source id="1" name="OpenMRS ID Generator" identifier_type="1" creator="1" date_created="2009-11-30 11:28:30.489" retired="false" uuid="0d47284f-9e9b-4a81-a88b-8bb42bc0a901" />
5151
<idgen_seq_id_gen id="1" first_identifier_base="1" next_sequence_value="6" base_character_set="abcdefghijklmnopqrstuvwxyz0123456789" prefix="" min_length="0" max_length="0" />
52-
52+
5353
<idgen_identifier_source id="2" name="Legacy ID Generator" identifier_type="2" creator="1" date_created="2009-11-30 11:28:30.489" retired="false" uuid="9eca4f4e-707f-4lb8-8289-2f9b6e93803f" />
5454
<idgen_seq_id_gen id="2" first_identifier_base="1" next_sequence_value="2" base_character_set="0123456789" prefix="" min_length="0" max_length="0" />
55-
55+
5656
<idgen_auto_generation_option id="15000" source="1" identifier_type="1" manual_entry_enabled="false" automatic_generation_enabled="true" uuid="eade77b6-3365-47ed-9ee3-2324598629eb" />
5757

5858
<relationship_type relationship_type_id="15001" a_is_to_b="Aunt" b_is_to_a="Niece" preferred="false" weight="0" description="A relationship of an aunt and her niece" creator="1" date_created="2007-05-04 00:00:00.0" retired="false" uuid="3982f469-cedc-4b2d-91ea-fe38f881e1a0"/>

0 commit comments

Comments
 (0)