From 67e7a2059a790bd039968a6927a5db9dd177c554 Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 09:59:23 -0500 Subject: [PATCH 01/26] GEPS045 Place Enhancements --- data/grampsxml.dtd | 53 +- data/grampsxml.rng | 38 +- gramps/gen/db/base.py | 35 +- gramps/gen/db/generic.py | 73 ++- gramps/gen/db/upgrade.py | 84 ++- gramps/gen/lib/__init__.py | 4 + gramps/gen/lib/attrtype.py | 15 + gramps/gen/lib/eventroletype.py | 10 +- gramps/gen/lib/eventtype.py | 13 +- gramps/gen/lib/place.py | 511 +++++++++++++---- gramps/gen/lib/placeabbrev.py | 175 ++++++ gramps/gen/lib/placeabbrevtype.py | 60 ++ gramps/gen/lib/placegrouptype.py | 81 +++ gramps/gen/lib/placehiertype.py | 81 +++ gramps/gen/lib/placename.py | 125 +++-- gramps/gen/lib/placeref.py | 112 ++-- gramps/gen/lib/placetype.py | 515 ++++++++++++++++-- gramps/gen/merge/mergeeventquery.py | 8 +- gramps/gen/proxy/private.py | 1 + gramps/gen/utils/db.py | 26 +- gramps/gen/utils/unknown.py | 8 +- gramps/gui/autocomp.py | 2 +- gramps/gui/editors/__init__.py | 2 + gramps/gui/editors/displaytabs/__init__.py | 4 + .../displaytabs/placeabbrevembedlist.py | 115 ++++ .../editors/displaytabs/placeabbrevmodel.py | 50 ++ .../editors/displaytabs/placeattrembedlist.py | 43 ++ .../displaytabs/placeeventembedlist.py | 356 ++++++++++++ .../editors/displaytabs/placenameembedlist.py | 14 +- .../gui/editors/displaytabs/placenamemodel.py | 13 +- .../editors/displaytabs/placerefembedlist.py | 5 +- .../gui/editors/displaytabs/placerefmodel.py | 5 +- .../editors/displaytabs/placetypeembedlist.py | 121 ++++ .../gui/editors/displaytabs/placetypemodel.py | 51 ++ gramps/gui/editors/editplace.py | 168 ++++-- gramps/gui/editors/editplaceabbrev.py | 97 ++++ gramps/gui/editors/editplacename.py | 34 +- gramps/gui/editors/editplaceref.py | 174 ++++-- gramps/gui/editors/editplacetype.py | 107 ++++ gramps/gui/glade/editplace.glade | 385 ++++++++----- gramps/gui/glade/editplaceabbrev.glade | 172 ++++++ gramps/gui/glade/editplacename.glade | 24 +- gramps/gui/glade/editplaceref.glade | 512 ++++++++++------- gramps/gui/glade/editplacetype.glade | 229 ++++++++ gramps/gui/glade/mergeplace.glade | 88 +-- gramps/gui/merge/mergeplace.py | 18 +- gramps/gui/selectors/selectplace.py | 7 +- gramps/gui/views/treemodels/placemodel.py | 36 +- gramps/gui/widgets/__init__.py | 1 + gramps/gui/widgets/monitoredwidgets.py | 7 +- gramps/gui/widgets/placetypeselector.py | 226 ++++++++ gramps/plugins/export/exportxml.py | 103 +++- gramps/plugins/gramplet/citations.py | 24 + gramps/plugins/gramplet/events.py | 74 ++- gramps/plugins/gramplet/gramplet.gpr.py | 14 + gramps/plugins/gramplet/locations.py | 11 +- gramps/plugins/gramplet/placedetails.py | 39 +- gramps/plugins/importer/importxml.py | 177 ++++-- gramps/plugins/lib/libgrampsxml.py | 2 +- gramps/plugins/lib/libplaceimport.py | 25 +- gramps/plugins/lib/libplaceview.py | 23 +- gramps/plugins/lib/maps/geography.py | 42 +- gramps/plugins/lib/maps/markerlayer.py | 2 +- gramps/plugins/lib/maps/placeselection.py | 25 +- gramps/plugins/mapservices/eniroswedenmap.py | 16 +- gramps/plugins/textreport/placereport.py | 2 +- gramps/plugins/tool/check.py | 29 +- gramps/plugins/view/eventview.py | 19 +- gramps/plugins/view/geoplaces.py | 167 +----- gramps/plugins/view/placetreeview.py | 2 + 70 files changed, 4691 insertions(+), 1199 deletions(-) create mode 100644 gramps/gen/lib/placeabbrev.py create mode 100644 gramps/gen/lib/placeabbrevtype.py create mode 100644 gramps/gen/lib/placegrouptype.py create mode 100644 gramps/gen/lib/placehiertype.py create mode 100644 gramps/gui/editors/displaytabs/placeabbrevembedlist.py create mode 100644 gramps/gui/editors/displaytabs/placeabbrevmodel.py create mode 100644 gramps/gui/editors/displaytabs/placeattrembedlist.py create mode 100644 gramps/gui/editors/displaytabs/placeeventembedlist.py create mode 100644 gramps/gui/editors/displaytabs/placetypeembedlist.py create mode 100644 gramps/gui/editors/displaytabs/placetypemodel.py create mode 100644 gramps/gui/editors/editplaceabbrev.py create mode 100644 gramps/gui/editors/editplacetype.py create mode 100644 gramps/gui/glade/editplaceabbrev.glade create mode 100644 gramps/gui/glade/editplacetype.glade create mode 100644 gramps/gui/widgets/placetypeselector.py diff --git a/data/grampsxml.dtd b/data/grampsxml.dtd index 160d4add5b2..366a6e6d5a4 100644 --- a/data/grampsxml.dtd +++ b/data/grampsxml.dtd @@ -24,15 +24,15 @@ --> @@ -58,10 +58,10 @@ DATABASE tags --> - - + + + False dialog + + + True @@ -87,7 +90,7 @@ False True - start + end _Title: True center @@ -95,31 +98,17 @@ 0 - 1 - - - - - True - True - start - Either use the two fields below to enter coordinates (latitude and longitude), - True - - - 0 - 3 - 5 + 0 - + True False - center + end L_atitude: True - center + right lat_entry @@ -128,17 +117,16 @@ - + True False start _Longitude: True center - lon_entry - 2 + 4 4 @@ -152,18 +140,17 @@ 1 - 1 - 4 + 0 + 5 - + True False - start + end _ID: True - gid 0 @@ -183,27 +170,14 @@ You can set these values via the Geography View by searching the place, or via a 1 4 - - - - - True - True - Longitude (position relative to the Prime, or Greenwich, Meridian) of the place in decimal or degree notation. -Eg, valid values are -124.3647, 124°52′21.92″E, E124°52′21.92″ or 124:52:21.92 -You can set these values via the Geography View by searching the place, or via a map service in the place view. - True - - - - 3 - 4 + 3 True False + start 6 @@ -221,9 +195,9 @@ You can set these values via the Geography View by searching the place, or via a - 0 + 1 5 - 4 + 5 @@ -246,64 +220,10 @@ You can set these values via the Geography View by searching the place, or via a - - 0 - 6 - 4 - - - - - True - False - 6 - - - 75 - True - True - A unique ID to identify the place - - 6 - - - False - True - 0 - - - - - True - False - start - Code: - True - - - False - True - 1 - - - - - True - True - Code associated with this place. Eg Country Code or Postal Code. - - 8 - - - True - True - 2 - - - 1 - 7 + 6 + 5 @@ -311,6 +231,7 @@ You can set these values via the Geography View by searching the place, or via a True True True + end none @@ -332,8 +253,8 @@ You can set these values via the Geography View by searching the place, or via a - 4 - 2 + 6 + 0 @@ -341,9 +262,10 @@ You can set these values via the Geography View by searching the place, or via a True True True + end - 4 + 6 7 @@ -351,23 +273,23 @@ You can set these values via the Geography View by searching the place, or via a True False - start + end place|Name: 0 - 2 + 1 - + True False - start + end Type: - 2 + 0 2 @@ -380,65 +302,131 @@ You can set these values via the Geography View by searching the place, or via a True + The type of this place . True True - 3 + 1 2 + 2 - + True False start - Tags: + 6 + + + - 2 - 7 + 0 + 0 + 5 - + + 37 True - False + True + True + Invoke place type editor. start - True + + + True + False + gtk-edit + + 3 - 7 + 2 - + + True + True + True + Invoke place name editor. + end + + + True + False + gtk-edit + + + + + 6 + 1 + + + + + True + True + The name of this place. + True + + + + 1 + 1 + 5 + + + + + True + False + What type of place this is. Eg 'Country', 'City', ... . + True + + + True + The type of this place . + True + + + + + 5 + 2 + + + + True False start - 6 - - - + Category: - 0 - 0 - 5 + 4 + 2 - + True False - + True True - The name of this place. + Longitude (position relative to the Prime, or Greenwich, Meridian) of the place in decimal or degree notation. +Eg, valid values are -124.3647, 124°52′21.92″E, E124°52′21.92″ or 124:52:21.92 +You can set these values via the Geography View by searching the place, or via a map service in the place view. True @@ -448,32 +436,147 @@ You can set these values via the Geography View by searching the place, or via a 0 + + + 5 + 4 + + + + + True + False + start - + True True - True - Invoke place name editor. - - - True - False - gtk-edit - - + start + Either use the two fields below to enter coordinates (latitude and longitude), False True - 1 + 0 1 + 3 + 6 + + + + + True + False + end + True + gid + + + 6 2 + + + True + False + end + True + gid + + + 6 + 4 + + + + + True + False + end + True + gid + + + 6 + 6 + + + + + True + False + end + True + gid + + + 6 + 5 + + + + + True + False + 6 + + + True + True + A unique ID to identify the place + + 18 + + + False + True + 0 + + + + + True + False + start + Tags: + + + False + True + 1 + + + + + True + False + start + True + + + False + True + 2 + + + + + 1 + 7 + 5 + + + + + + + + @@ -488,6 +591,8 @@ You can set these values via the Geography View by searching the place, or via a True True + True + True diff --git a/gramps/gui/glade/editplaceabbrev.glade b/gramps/gui/glade/editplaceabbrev.glade new file mode 100644 index 00000000000..9e999c8f764 --- /dev/null +++ b/gramps/gui/glade/editplaceabbrev.glade @@ -0,0 +1,172 @@ + + + + + + + False + dialog + + + + + + True + False + vertical + + + True + False + end + + + _Cancel + True + True + True + True + True + + + False + False + 0 + + + + + _OK + True + True + True + True + True + True + Accept changes and close window + Accept changes and close window + True + + + False + False + 1 + + + + + _Help + True + True + True + True + True + + + False + False + 2 + + + + + False + True + end + 0 + + + + + True + False + vertical + + + True + False + 12 + 6 + 12 + + + True + False + start + Type: + True + center + + + 0 + 1 + + + + + True + True + The abbreviation of the place. + True + + + + 1 + 0 + + + + + True + False + start + Abbreviation: + True + center + + + 0 + 0 + + + + + True + False + What type of abbreviation this is. Eg 'ISO3166', ... . + True + + + True + True + + + + + 1 + 1 + + + + + False + True + 0 + + + + + True + True + 2 + + + + + + cancel + ok + help + + + diff --git a/gramps/gui/glade/editplacename.glade b/gramps/gui/glade/editplacename.glade index 8b012d0b3f2..1e3c0c222d2 100644 --- a/gramps/gui/glade/editplacename.glade +++ b/gramps/gui/glade/editplacename.glade @@ -1,11 +1,14 @@ - + False dialog + + + True @@ -215,11 +218,28 @@ 0 + + + True + True + + + + + + + + + True + True + 1 + + True True - 1 + 2 diff --git a/gramps/gui/glade/editplaceref.glade b/gramps/gui/glade/editplaceref.glade index 925859f2ee7..eb85f562970 100644 --- a/gramps/gui/glade/editplaceref.glade +++ b/gramps/gui/glade/editplaceref.glade @@ -1,5 +1,5 @@ - + @@ -7,6 +7,9 @@ False 600 dialog + + + True @@ -106,7 +109,7 @@ True False 12 - 6 + 12 12 @@ -128,6 +131,8 @@ True True + Date range in which the enclosure is valid. + True 1 @@ -139,6 +144,8 @@ True True True + Invoke date editor + start True @@ -152,6 +159,37 @@ 0 + + + True + False + What type of hierarchy is this ('Administrative', 'Religious'), ... . + True + True + + + True + True + + + + + 4 + 0 + + + + + True + False + start + Hierarchy Type: + + + 3 + 0 + + @@ -178,7 +216,7 @@ True True - 6 + 20 True 6 @@ -186,162 +224,107 @@ True True False + True + True - + True False 12 - 6 - 12 + 4 + 6 False True - start - Title: + end + _Title: True center + place_title 0 - 1 + 0 - + True False - start - Name: + end + L_atitude: True - center - - - 0 - 2 - - - - - True - True - start - Either use the two fields below to enter coordinates (latitude and longitude), - True + right + lat_entry 0 - 3 - 5 + 4 - + True False start - 3 - 3 - ID: + _Longitude: True center - 0 - 7 - - - - - True - False - start - Latitude: - True - - - 0 + 4 4 - - False - 6 - 12 - - - True - False - start - 48 - dialog-warning - 6 - - - False - False - 0 - - - - - 500 - True - False - start - 4 - 4 - <b>Note:</b> Any changes in the enclosing place information will be reflected in the place itself, for places that it encloses. - True - fill - True - - - True - True - 1 - - + + True + True + Full title of this place. + True + - 0 - 8 + 1 + 0 5 - + True False - start - Type: + end + _ID: True - center - 2 - 2 + 0 + 7 - + True - False - start - Longitude: - True - center + True + Latitude (position above the Equator) of the place in decimal or degree notation. +Eg, valid values are 12.0154, 50°52′21.92″N, N50°52′21.92″ or 50:52:21.92 +You can set these values via the Geography View by searching the place, or via a map service in the place view. + True + - 2 + 1 4 + 3 True False + start 6 @@ -359,9 +342,9 @@ - 0 + 1 5 - 4 + 5 @@ -385,28 +368,9 @@ - 0 + 1 6 - 4 - - - - - True - True - What type of place this is. Eg 'Country', 'City', ... . - True - True - - - True - True - - - - - 3 - 2 + 5 @@ -414,6 +378,7 @@ True True True + end none @@ -435,67 +400,181 @@ - 4 - 4 + 6 + 0 - + + True True - True - Full title of this place. - True - + True + end + + + 6 + 7 + + + + + True + False + end + place|Name: + + + 0 + 1 + + + + + True + False + end + Type: + + + 0 + 2 + + + + + True + False + What type of place this is. Eg 'Country', 'City', ... . + True + + + True + The type of this place . + True + + 1 + 2 + 2 + + + + + True + False + start + 6 + + + + + + 0 + 0 + 5 + + + + + 37 + True + True + True + Invoke place type editor. + start + + + True + False + gtk-edit + + + + + 3 + 2 + + + + + True + True + True + Invoke place name editor. + end + + + True + False + gtk-edit + + + + + 6 1 - 4 - + True True - Latitude (position above the Equator) of the place in decimal or degree notation. -Eg, valid values are 12.0154, 50°52′21.92″N, N50°52′21.92″ or 50:52:21.92 -You can set these values via the Geography View by searching the place, or via a map service in the place view. + The name of this place. True 1 - 4 + 1 + 5 - + True - True - Longitude (position relative to the Prime, or Greenwich, Meridian) of the place in decimal or degree notation. -Eg, valid values are -124.3647, 124°52′21.92″E, E124°52′21.92″ or 124:52:21.92 -You can set these values via the Geography View by searching the place, or via a map service in the place view. - True - + False + What type of place this is. Eg 'Country', 'City', ... . + True + + + True + The type of this place . + True + + - 3 - 4 + 5 + 2 - + + True + False + start + Category: + + + 4 + 2 + + + + True False - 6 - - 75 + True True - A unique ID to identify the place + Longitude (position relative to the Prime, or Greenwich, Meridian) of the place in decimal or degree notation. +Eg, valid values are -124.3647, 124°52′21.92″E, E124°52′21.92″ or 124:52:21.92 +You can set these values via the Geography View by searching the place, or via a map service in the place view. + True - 6 False @@ -503,102 +582,101 @@ You can set these values via the Geography View by searching the place, or via a 0 + + + 5 + 4 + + + + + True + False + start - + True False start - Code: - True + Either use the two fields below to enter coordinates (latitude and longitude), False True - 1 - - - - - True - True - Code associated with this place. Eg Country Code or Postal Code. - - 8 - - - True - True - 2 + 0 1 - 7 + 3 + 6 - + True False - start - Tags: + end + True + gid - 2 - 7 + 6 + 2 - + True False - start - True + end + True + gid - 3 - 7 + 6 + 4 - + True - True - True + False + end + True + gid - 4 - 7 + 6 + 6 - + True False - start - 6 - - - + end + True + gid - 0 - 0 - 5 + 6 + 5 - + True False + 6 - + True True - The name of this place. - True + A unique ID to identify the place + 18 False @@ -607,18 +685,11 @@ You can set these values via the Geography View by searching the place, or via a - + True - True - True - - - True - False - Invoke place name editor. - gtk-edit - - + False + start + Tags: False @@ -626,19 +697,36 @@ You can set these values via the Geography View by searching the place, or via a 1 + + + True + False + start + True + + + False + True + 2 + + 1 - 2 + 7 + 5 + + + + + + - - False - diff --git a/gramps/gui/glade/editplacetype.glade b/gramps/gui/glade/editplacetype.glade new file mode 100644 index 00000000000..f09b7feaea5 --- /dev/null +++ b/gramps/gui/glade/editplacetype.glade @@ -0,0 +1,229 @@ + + + + + + + False + dialog + + + + + + True + False + vertical + + + True + False + end + + + _Cancel + True + True + True + True + True + + + False + False + 0 + + + + + _OK + True + True + True + True + True + True + Accept changes and close window + Accept changes and close window + True + + + False + False + 1 + + + + + _Help + True + True + True + True + True + + + False + False + 2 + + + + + False + True + end + 0 + + + + + True + False + vertical + + + True + False + 12 + 6 + 12 + + + True + False + start + _Date: + True + center + date_entry + + + 0 + 1 + + + + + True + True + True + True + Invoke date editor + none + + + True + False + gramps-date + + + Date + + + + + + + + + + Date + + + + + + 2 + 1 + + + + + True + True + Date range in which the name is valid. + True + + + + 1 + 1 + + + + + True + False + start + Type: + True + center + + + 0 + 0 + + + + + True + False + What type of abbreviation this is. Eg 'ISO3166', ... . + True + + + True + True + + + + + 1 + 0 + + + + + + + + False + True + 0 + + + + + True + True + True + + + + + + + + + True + True + 1 + + + + + True + True + 2 + + + + + + cancel + ok + help + + + diff --git a/gramps/gui/glade/mergeplace.glade b/gramps/gui/glade/mergeplace.glade index c79cee2237f..40e27572328 100644 --- a/gramps/gui/glade/mergeplace.glade +++ b/gramps/gui/glade/mergeplace.glade @@ -1,11 +1,14 @@ - + False True dialog + + + True @@ -243,7 +246,7 @@ primary data for the merged place. 0 - 5 + 4 @@ -259,7 +262,7 @@ primary data for the merged place. 2 - 5 + 4 @@ -274,7 +277,7 @@ primary data for the merged place. 0 - 6 + 5 @@ -290,7 +293,7 @@ primary data for the merged place. 2 - 6 + 5 @@ -305,7 +308,7 @@ primary data for the merged place. 0 - 7 + 6 @@ -321,7 +324,7 @@ primary data for the merged place. 2 - 7 + 6 @@ -357,7 +360,7 @@ primary data for the merged place. 1 - 5 + 4 @@ -369,7 +372,7 @@ primary data for the merged place. 3 - 5 + 4 @@ -381,7 +384,7 @@ primary data for the merged place. 1 - 6 + 5 @@ -393,7 +396,7 @@ primary data for the merged place. 3 - 6 + 5 @@ -405,7 +408,7 @@ primary data for the merged place. 1 - 7 + 6 @@ -417,7 +420,7 @@ primary data for the merged place. 3 - 7 + 6 @@ -540,65 +543,6 @@ primary data for the merged place. 3 - - - Code: - True - True - False - start - half - True - True - True - - - 0 - 4 - - - - - Code: - True - True - False - start - True - True - code_btn1 - - - 2 - 4 - - - - - True - True - True - False - - - - 1 - 4 - - - - - True - True - True - False - - - - 3 - 4 - - diff --git a/gramps/gui/merge/mergeplace.py b/gramps/gui/merge/mergeplace.py index 5c0309cfe95..f6a88a144b4 100644 --- a/gramps/gui/merge/mergeplace.py +++ b/gramps/gui/merge/mergeplace.py @@ -112,14 +112,6 @@ def __init__(self, dbstate, uistate, track, handle1, handle2, for widget_name in ('type1', 'type2', 'type_btn1', 'type_btn2'): self.get_widget(widget_name).set_sensitive(False) - entry1 = self.get_widget("code1") - entry2 = self.get_widget("code2") - entry1.set_text(self.pl1.get_code()) - entry2.set_text(self.pl2.get_code()) - if entry1.get_text() == entry2.get_text(): - for widget_name in ('code1', 'code2', 'code_btn1', 'code_btn2'): - self.get_widget(widget_name).set_sensitive(False) - entry1 = self.get_widget("lat1") entry2 = self.get_widget("lat2") entry1.set_text(self.pl1.get_latitude()) @@ -153,8 +145,10 @@ def __init__(self, dbstate, uistate, track, handle1, handle2, rbutton1 = self.get_widget("handle_btn1") rbutton_label1 = self.get_widget("label_handle_btn1") rbutton_label2 = self.get_widget("label_handle_btn2") - rbutton_label1.set_label(title1 + " [" + gramps1 + "] " + str(self.pl1.place_type)) - rbutton_label2.set_label(title2 + " [" + gramps2 + "] " + str(self.pl2.place_type)) + rbutton_label1.set_label(title1 + " [" + gramps1 + "] " + + str(self.pl1.get_type())) + rbutton_label2.set_label(title2 + " [" + gramps2 + "] " + + str(self.pl2.get_type())) rbutton1.connect("toggled", self.on_handle1_toggled) self.connect_button('place_help', self.cb_help) @@ -168,7 +162,6 @@ def on_handle1_toggled(self, obj): self.get_widget("title_btn1").set_active(True) self.get_widget("name_btn1").set_active(True) self.get_widget("type_btn1").set_active(True) - self.get_widget("code_btn1").set_active(True) self.get_widget("lat_btn1").set_active(True) self.get_widget("long_btn1").set_active(True) self.get_widget("gramps_btn1").set_active(True) @@ -176,7 +169,6 @@ def on_handle1_toggled(self, obj): self.get_widget("title_btn2").set_active(True) self.get_widget("name_btn2").set_active(True) self.get_widget("type_btn2").set_active(True) - self.get_widget("code_btn2").set_active(True) self.get_widget("lat_btn2").set_active(True) self.get_widget("long_btn2").set_active(True) self.get_widget("gramps_btn2").set_active(True) @@ -204,8 +196,6 @@ def cb_merge(self, obj): phoenix.set_name(titanic.get_name()) if self.get_widget("type_btn1").get_active() ^ use_handle1: phoenix.set_type(titanic.get_type()) - if self.get_widget("code_btn1").get_active() ^ use_handle1: - phoenix.set_code(titanic.get_code()) if self.get_widget("lat_btn1").get_active() ^ use_handle1: phoenix.set_latitude(titanic.get_latitude()) if self.get_widget("long_btn1").get_active() ^ use_handle1: diff --git a/gramps/gui/selectors/selectplace.py b/gramps/gui/selectors/selectplace.py index 477bd56b37a..fe349804814 100644 --- a/gramps/gui/selectors/selectplace.py +++ b/gramps/gui/selectors/selectplace.py @@ -68,7 +68,7 @@ def get_column_titles(self): (_('ID'), 75, BaseSelector.TEXT, 1), (_('Type'), 100, BaseSelector.TEXT, 3), (_('Title'), 300, BaseSelector.TEXT, 2), - (_('Last Change'), 150, BaseSelector.TEXT, 9), + (_('Last Change'), 150, BaseSelector.TEXT, 8), ] def get_from_handle_func(self): @@ -77,10 +77,9 @@ def get_from_handle_func(self): def setup_filter(self): """Build the default filters and add them to the filter menu. This overrides the baseselector method because we use the hidden - COL_SEARCH (11) that has alt names as well as primary name for name - searching""" + COL_SEARCH (10) that has all names for name searching""" cols = [(pair[3], - pair[1] if pair[1] else 11, + pair[1] if pair[1] else 10, pair[0] in self.exact_search()) for pair in self.column_order() if pair[0] ] diff --git a/gramps/gui/views/treemodels/placemodel.py b/gramps/gui/views/treemodels/placemodel.py index c4b47e09c8b..8a06d7556fd 100644 --- a/gramps/gui/views/treemodels/placemodel.py +++ b/gramps/gui/views/treemodels/placemodel.py @@ -44,7 +44,7 @@ # Gramps modules # #------------------------------------------------------------------------- -from gramps.gen.lib import Place, PlaceType +from gramps.gen.lib import Place, PlaceType, PlaceGroupType from gramps.gen.datehandler import format_time from gramps.gen.utils.place import conv_lat_lon from gramps.gen.display.place import displayer as place_displayer @@ -75,12 +75,12 @@ def __init__(self, db): self.column_id, self.column_title, self.column_type, - self.column_code, self.column_latitude, self.column_longitude, self.column_private, self.column_tags, self.column_change, + self.column_group, self.column_tag_color, self.search_name, ] @@ -89,12 +89,12 @@ def __init__(self, db): self.column_id, self.column_title, self.column_type, - self.column_code, self.sort_latitude, self.sort_longitude, self.column_private, self.column_tags, self.sort_change, + self.column_group, self.column_tag_color, self.search_name, ] @@ -130,12 +130,12 @@ def column_title(self, data): def column_name(self, data): """ Return the primary name """ - return data[6][0] + return data[6][0][0] def search_name(self, data): - """ The search name includes all alt names to enable finding by alt name + """ The search name includes all names to enable finding """ - return ','.join([data[6][0]] + [name[0] for name in data[7]]) + return ','.join([name[0] for name in data[6]]) def column_longitude(self, data): if not data[3]: @@ -173,23 +173,23 @@ def column_id(self, data): return data[1] def column_type(self, data): - return str(PlaceType(data[8])) - - def column_code(self, data): - return data[9] + ptdata = data[7][0] + return str(PlaceType((ptdata[0], ptdata[1]))) def column_private(self, data): - if data[17]: + if data[16]: return 'gramps-lock' - else: - # There is a problem returning None here. - return '' + # There is a problem returning None here. + return '' def sort_change(self, data): - return "%012x" % data[15] + return "%012x" % data[14] + + def column_group(self, data): + return str(PlaceGroupType(data[17])) def column_change(self, data): - return format_time(data[15]) + return format_time(data[14]) def get_tag_name(self, tag_handle): """ @@ -210,7 +210,7 @@ def column_tag_color(self, data): if not cached: tag_color = "" tag_priority = None - for handle in data[16]: + for handle in data[15]: tag = self.db.get_tag_from_handle(handle) if tag: this_priority = tag.get_priority() @@ -225,7 +225,7 @@ def column_tags(self, data): """ Return the sorted list of tags. """ - tag_list = list(map(self.get_tag_name, data[16])) + tag_list = list(map(self.get_tag_name, data[15])) # TODO for Arabic, should the next line's comma be translated? return ', '.join(sorted(tag_list, key=glocale.sort_key)) diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index b12eec59bf2..b6025178f57 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -43,3 +43,4 @@ from .validatedcomboentry import * from .validatedmaskedentry import * from .placewithin import * +from .placetypeselector import * diff --git a/gramps/gui/widgets/monitoredwidgets.py b/gramps/gui/widgets/monitoredwidgets.py index 3b694f69025..97c6c41bdaf 100644 --- a/gramps/gui/widgets/monitoredwidgets.py +++ b/gramps/gui/widgets/monitoredwidgets.py @@ -396,7 +396,7 @@ class MonitoredDataType: def __init__(self, obj, set_val, get_val, readonly=False, - custom_values=None, ignore_values=None): + custom_values=None, ignore_values=None, changed=None): """ Constructor for the MonitoredDataType class. @@ -415,11 +415,14 @@ def __init__(self, obj, set_val, get_val, readonly=False, :param ignore_values: list of values not to show in the combobox. If the result of get_val is in these, it is not ignored :type ignore_values: list of int + :param changed: To update an external element when we change value + :type callback: method """ self.set_val = set_val self.get_val = get_val self.obj = obj + self.changed = changed val = get_val() @@ -475,6 +478,8 @@ def update(self): def on_change(self, obj): value = self.fix_value(self.sel.get_values()) self.set_val(value) + if self.changed: + self.changed(obj) #------------------------------------------------------------------------- # diff --git a/gramps/gui/widgets/placetypeselector.py b/gramps/gui/widgets/placetypeselector.py new file mode 100644 index 00000000000..97b3d69516c --- /dev/null +++ b/gramps/gui/widgets/placetypeselector.py @@ -0,0 +1,226 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2015 Nick Hall +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +__all__ = ["PlaceTypeSelector"] + +from gi.repository import Gtk +#------------------------------------------------------------------------- +# +# Standard python modules +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".widgets.placetypeselector") + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from gramps.gen.lib import PlaceType +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext + + +#------------------------------------------------------------------------- +# +# PlaceTypeSelector class +# +#------------------------------------------------------------------------- +class PlaceTypeSelector(): + """ Class that sets up a comboentry for the place types """ + + def __init__(self, dbstate, combo, ptype, changed=None, sidebar=False): + """ + Constructor for the PlaceTypeSelector class. + + :param combo: Existing ComboBox widget to use with has_entry=True. + :type combo: Gtk.ComboBox + :param ptyperef: The object to fill/modify + :type ptype: PlaceType object + :param db: the database + :type db: based on DbReadBase, DbWriteBase + :param changed: To update an external element when we change value + :type callback: method + """ + self.ptype = ptype + self.changed = changed + self.combo = combo + self.dbstate = dbstate + self.sidebar = sidebar + # fill out completion + self.e_completion = Gtk.EntryCompletion() + self.e_completion.set_minimum_key_length(1) + self.e_completion.set_text_column(1) + self.e_completion.connect('match-selected', self.on_entry_change) + # following is used to indicate if an e_completion was selected + self.entry_valid = False + entry = combo.get_child() + entry.set_completion(self.e_completion) + entry.set_text(ptype.str(expand=True)) + + self.fill_models() # fill out the initial models + combo.set_entry_text_column(1) + + combo.connect('changed', self.on_combo_change) + + def fill_models(self, *_arg): + """ fill in the models with the current PlaceType data. This is used + at init and also when the user starts editing the sidebar filter. + """ + # get completion store and menu + store, menu = get_menu(self.dbstate.db) + self.e_completion.set_model(store) + # Create a model and fill it with a two or three-level tree + # corresponding to the menu. + # If the active key is in an items list, the group under that parent + # is expanded. + # Items not under a parent group are also supported. + self.store = self.combo.get_model() + if self.store: + # for some reason, the combo doesn't like to have its TreeModel + # replaced, so we need to clear it if it already exists. + self.store.clear() + else: + self.store = Gtk.TreeStore(str, str) + self.combo.set_model(self.store) + for (heading, items) in menu: + if not heading: # add ptype in items if expand + parent = None + else: + parent = self.store.append(None, row=[None, _(heading)]) + for item in items: + if not isinstance(item[1], list): + self.store.append(parent, row=list(item)) + continue + heading_2, items_2 = item + parent_2 = self.store.append(parent, row=[None, heading_2]) + for item_2 in items_2: + self.store.append(parent_2, row=list(item_2)) + if not self.sidebar: + self.combo.set_sensitive(not self.dbstate.db.readonly) + + def on_combo_change(self, combo): + """ Deal with changes in the combo or entry; put results in the + PlaceType + """ + active_iter = combo.get_active_iter() + if active_iter: # selected from menu + self.ptype.set((self.store.get_value(active_iter, 0), # pt_id + self.store.get_value(active_iter, 1))) # name + elif not self.entry_valid: # custom value entered + self.ptype.set((PlaceType.CUSTOM, + combo.get_child().get_text().strip())) + else: # selected an entry completion item, so don't modify here + return + if self.changed: + self.changed() + + def on_entry_change(self, _entrycompletion, model, active_iter): + """ Deal with changes in the combo or entry; put results in the + PlaceType + """ + if active_iter: + self.ptype.set((model.get_value(active_iter, 0), # pt_id + model.get_value(active_iter, 1))) # name + self.entry_valid = True + if self.changed: + self.changed() + + def update(self): + """ An external change to self.ptype needs to be reflected + into the combo. + """ + if self.ptype is not None: + self.combo.get_child().set_text(self.ptype.str(expand=True)) + + +def get_menu(db): + """ This creates a place type menu structure suitable for + StandardCustomSelector. + It processes the DATAMAP and db data into a menu. Both of these are + updated from the db as needed when types or groups are changed. + This is called by the StandardCustomSelector. + + :returns: A menu suitable for StandardCustomSelector. + :rtype: list + """ + def prepare(ptype): + """ prepare to use a ptype and format the name for the menu """ + if ptype != PlaceType.CUSTOM: # if allowed to show in menu + cats.update(ptype.countries.split()) + name = str(ptype) + ptype.name = name + if ptype.pt_id not in types: + store.append(row=[ptype.pt_id, name]) + types[ptype.pt_id] = ptype + menu = [] # the whole menu + store = Gtk.ListStore(str, str) # a completion list (pt_id, name) + items = [] # list if items in a menu heading + types = [] # list of key=hndl, data=PlaceTypes + cats = set() # set of catagories to display + # created integrated dict of types + for pt_id in PlaceType.DATAMAP: + if pt_id != PlaceType.CUSTOM: + ptype = PlaceType() + ptype.set(pt_id) + types.append((pt_id, ptype.str(expand=True), + ptype.get_countries())) + store.append(row=[pt_id, ptype.str(expand=True)]) + cats.update(ptype.get_countries().split()) + for ptype in db.get_place_types(): + # they are all CUSTOM + types.append((PlaceType.CUSTOM, ptype, '##')) + store.append(row=[PlaceType.CUSTOM, ptype]) + cats.add('##') + types = sorted([typ for typ in types], key=lambda typ: typ[1]) + + uncommon = False # used to avoid category sub menu entry when only common + # # items are present + for cat in sorted(cats): + categ = (_("Common") if cat == '!!' else + _("Custom") if cat == '##' else cat) + cont = False + _mt = True + items = [] + for pt_id, name, category in types: + # exclude UNKNOWN, hidden items, and CUSTOM + if cat in category: + items.append((pt_id, name)) + _mt = False + if len(items) == 18: # Add 'cont.' for large groups + if cont: + # translators: used to add levels to a menu + # as in "Items continued" but abbreviated for brevity + menu.append((_("%s cont.") % categ, items)) + else: + menu.append((categ, items)) + cont = True + items = [] + elif cat == '!!': + uncommon = True + if items and not _mt: + if cont: + menu.append((_("%s cont.") % categ, items)) + elif uncommon: + menu.append((categ, items)) + else: + menu.append((None, items)) + return store, menu diff --git a/gramps/plugins/export/exportxml.py b/gramps/plugins/export/exportxml.py index fac655b0882..c58e48eeebb 100644 --- a/gramps/plugins/export/exportxml.py +++ b/gramps/plugins/export/exportxml.py @@ -57,7 +57,7 @@ from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext from gramps.gen.const import URL_HOMEPAGE -from gramps.gen.lib import Date, Person +from gramps.gen.lib import Date, Person, PlaceType from gramps.gen.updatecallback import UpdateCallback from gramps.gen.db.exceptions import DbWriteFailure from gramps.version import VERSION @@ -732,29 +732,67 @@ def dump_event_ref(self,eventref,index=1): self.g.write('%s\n' % sp) def dump_place_ref(self, placeref, index=1): - sp = " " * index + """ ref, hierarchy type, date, citations """ + _sp = " " * index date = placeref.get_date_object() - if date.is_empty(): - self.write_ref('placeref', placeref.ref, index, close=True) + htype = ' type="%s"' % escxml(placeref.get_type().xml_str()) + if date.is_empty() and not placeref.get_citation_list(): + self.write_ref('placeref', placeref.ref, index, close=True, + extra_text=htype) + else: + self.write_ref('placeref', placeref.ref, index, close=False, + extra_text=htype) + if not date.is_empty(): + self.write_date(date, index + 1) + for citation_handle in placeref.get_citation_list(): + self.write_ref("citationref", citation_handle, index + 1) + self.g.write('%s\n' % _sp) + + def dump_place_type(self, placetype, index=1): + """ type, date, citations """ + _sp = " " * index + pt_id = self.fix(placetype.pt_id) + pt_name = self.fix(placetype.name) + self.g.write('%s\n') else: - self.write_ref('placeref', placeref.ref, index, close=False) - self.write_date(date, index+1) - self.g.write('%s\n' % sp) + self.g.write('>\n') + if not date.is_empty(): + self.write_date(date, index + 1) + for citation_handle in placetype.get_citation_list(): + self.write_ref("citationref", citation_handle, index + 1) + self.g.write('%s\n' % _sp) def dump_place_name(self, place_name, index=1): - sp = " " * index + """ name, date, lang, citations, abbreviations """ + _sp = " " * index value = place_name.get_value() date = place_name.get_date_object() lang = place_name.get_language() - self.g.write('%s\n') else: self.g.write('>\n') - self.write_date(date, index+1) - self.g.write('%s\n' % sp) + if not date.is_empty(): + self.write_date(date, index + 1) + for citation_handle in place_name.get_citation_list(): + self.write_ref("citationref", citation_handle, index + 1) + self.write_place_abbrev_list(place_name.get_abbrevs(), index + 1) + self.g.write('%s\n' % _sp) + + def write_place_abbrev_list(self, _list, indent=3): + """ list of type, value of abbreviation """ + _sp = ' ' * indent + for abbr in _list: + self.g.write('%s\n' % + (_sp, escxml(abbr.get_type().xml_str()), + self.fix(abbr.get_value()))) def write_event(self,event,index=1): if not event: @@ -1225,38 +1263,43 @@ def write_url_list(self, list, index=1): ) def write_place_obj(self, place, index=1): + """ ptypes, title, names, ptypes, lat/lon, placerefs, alt locs, medias, + urls, notes, citations, tags, attributes, eventrefs """ self.write_primary_tag("placeobj", place, index, close=False) - ptype = self.fix(place.get_type().xml_str()) - self.g.write(' type="%s"' % ptype) - self.g.write('>\n') - + self.g.write(' group="%s">\n' % self.fix(str(place.group))) title = self.fix(place.get_title()) - code = self.fix(place.get_code()) - self.write_line_nofix("ptitle", title, index+1) - self.write_line_nofix("code", code, index+1) + self.write_line_nofix("ptitle", title, index + 1) - self.dump_place_name(place.get_name(), index+1) - for pname in place.get_alternative_names(): - self.dump_place_name(pname, index+1) + for pname in place.get_names(): + self.dump_place_name(pname, index + 1) + + for ptype in place.get_types(): + self.dump_place_type(ptype, index + 1) longitude = self.fix(place.get_longitude()) lat = self.fix(place.get_latitude()) if longitude or lat: self.g.write('%s\n' - % (" "*(index+1), longitude, lat)) + % (" " * (index + 1), longitude, lat)) + for placeref in place.get_placeref_list(): - self.dump_place_ref(placeref, index+1) + self.dump_place_ref(placeref, index + 1) list(map(self.dump_location, place.get_alternate_locations())) - self.write_media_list(place.get_media_list(), index+1) - self.write_url_list(place.get_url_list()) - self.write_note_list(place.get_note_list(), index+1) + self.write_media_list(place.get_media_list(), index + 1) + self.write_url_list(place.get_url_list(), index + 1) + self.write_note_list(place.get_note_list(), index + 1) for citation_handle in place.get_citation_list(): - self.write_ref("citationref", citation_handle, index+1) + self.write_ref("citationref", citation_handle, index + 1) for tag_handle in place.get_tag_list(): - self.write_ref("tagref", tag_handle, index+1) + self.write_ref("tagref", tag_handle, index + 1) + + self.write_attribute_list(place.get_attribute_list()) + + for event_ref in place.get_event_ref_list(): + self.dump_event_ref(event_ref, index + 1) - self.g.write("%s\n" % (" "*index)) + self.g.write("%s\n" % (" " * index)) def write_object(self, obj, index=1): self.write_primary_tag("object", obj, index) diff --git a/gramps/plugins/gramplet/citations.py b/gramps/plugins/gramplet/citations.py index 12902f919e8..b93e4221129 100644 --- a/gramps/plugins/gramplet/citations.py +++ b/gramps/plugins/gramplet/citations.py @@ -132,7 +132,14 @@ def add_event_citations(self, event): def add_place_citations(self, place): self.callman.register_handles({'place': [place.handle]}) self.add_citations(place) + self.add_attribute_citations(place) self.add_mediaref_citations(place) + for place_ref in place.get_placeref_list(): + self.add_citations(place_ref) + for name in place.get_names(): + self.add_citations(name) + for _type in place.get_types(): + self.add_citations(_type) def add_address_citations(self, obj): for address in obj.get_address_list(): @@ -234,8 +241,19 @@ def check_event_citations(self, event): def check_place_citations(self, place): if self.check_citations(place): return True + if self.check_attribute_citations(place): + return True if self.check_mediaref_citations(place): return True + for place_ref in place.get_placeref_list(): + if self.check_citations(place_ref): + return True + for name in place.get_names(): + if self.check_citations(name): + return True + for _type in place.get_types(): + if self.check_citations(_type): + return True return False def check_address_citations(self, obj): @@ -518,6 +536,8 @@ def display_citations(self, place): """ self.source_nodes = {} self.add_place_citations(place) + self.add_eventref_citations(place) + self.add_attribute_citations(place) self.set_has_data(self.model.count > 0) self.model.tree.expand_all() @@ -529,6 +549,10 @@ def get_has_data(self, place): return False if self.check_place_citations(place): return True + if self.check_eventref_citations(place): + return True + if self.check_attribute_citations(place): + return True return False class MediaCitations(Citations): diff --git a/gramps/plugins/gramplet/events.py b/gramps/plugins/gramplet/events.py index f857885fd78..c094d73ca1d 100644 --- a/gramps/plugins/gramplet/events.py +++ b/gramps/plugins/gramplet/events.py @@ -56,7 +56,7 @@ def __init__(self, gui, nav_group=0): self.db = None """ - Displays the events for a person or family. + Displays the events for a person, place, or family. """ def init(self): self.gui.WIDGET = self.build_gui() @@ -303,3 +303,75 @@ def get_start_date(self): event = get_marriage_or_fallback(self.db, active) return event.get_date_object() if event else None + +class PlaceEvents(Events): + """ + Displays the events for a place. + """ + def db_changed(self): + self.connect(self.dbstate.db, 'place-update', self.update) + self.connect_signal('Place', self.update) + + def update_has_data(self): + active_handle = self.get_active('Place') + active = None + if active_handle: + active = self.dbstate.db.get_place_from_handle(active_handle) + self.set_has_data(self.get_has_data(active)) + + @staticmethod + def get_has_data(active_place): + """ + Return True if the gramplet has data, else return False. + """ + if active_place: + if active_place.get_event_ref_list(): + return True + return False + + def main(self): # return false finishes + active_handle = self.get_active('Place') + + self.model.clear() + self.callman.unregister_all() + if active_handle: + self.display_place(active_handle) + else: + self.set_has_data(False) + + def display_place(self, active_handle): + """ + Display the events for the active place. + """ + active_place = self.dbstate.db.get_place_from_handle(active_handle) + self.cached_start_date = self.get_start_date() + for event_ref in active_place.get_event_ref_list(): + self.add_event_ref(event_ref) + self.set_has_data(self.model.count > 0) + + def get_start_date(self): + """ + Get the start date for a place, usually a marriage date, or + something close to marriage. + """ + return None + + def build_gui(self): + """ + Build the GUI interface. + """ + tip = _('Double-click on a row to edit the selected event.') + self.set_tooltip(tip) + top = Gtk.TreeView() + titles = [('', NOSORT, 50,), + (_('Type'), 1, 100), + (_('Description'), 2, 150), + (_('Date'), 3, 100), + ('', NOSORT, 50), + ('', NOSORT, 50), + ('', NOSORT, 50), + (_('Place'), 5, 400), + (_('Main Participants'), 6, 200), + (_('Role'), 7, 100)] + self.model = ListModel(top, titles, event_func=self.edit_event) + return top diff --git a/gramps/plugins/gramplet/gramplet.gpr.py b/gramps/plugins/gramplet/gramplet.gpr.py index 16bf3191ec5..fa14c978b26 100644 --- a/gramps/plugins/gramplet/gramplet.gpr.py +++ b/gramps/plugins/gramplet/gramplet.gpr.py @@ -492,6 +492,20 @@ navtypes=["Family"], ) +register(GRAMPLET, + id="Place Events", + name=_("Place Events"), + description = _("Gramplet showing the events for a place"), + version="1.0.0", + gramps_target_version=MODULE_VERSION, + status = STABLE, + fname="events.py", + height=200, + gramplet = 'PlaceEvents', + gramplet_title=_("Events"), + navtypes=["Place"], + ) + register(GRAMPLET, id="Person Gallery", name=_("Person Gallery"), diff --git a/gramps/plugins/gramplet/locations.py b/gramps/plugins/gramplet/locations.py index d6096095920..fa2f9fa5dbb 100644 --- a/gramps/plugins/gramplet/locations.py +++ b/gramps/plugins/gramplet/locations.py @@ -82,10 +82,11 @@ def build_gui(self): self.set_tooltip(tip) top = Gtk.TreeView() titles = [('', 0, 50), - (_('Name'), 1, 300), - (_('Type'), 2, 150), - (_('Date'), 5, 250), - (_('ID'), 4, 100), + (_('Name'), 1, 250), + (_('Type'), 2, 100), + (_('Date'), 5, 225), + (_('ID'), 4, 75), + (_('Hierarchy'), 5, 100), ('', NOSORT, 50)] self.model = ListModel(top, titles, list_mode="tree", event_func=self.edit_place) @@ -133,6 +134,7 @@ def add_place(self, placeref, place, node, visited, drange): """ place_date = get_date(placeref) place_sort = '%012d' % placeref.get_date_object().get_sort_value() + place_hier = str(placeref.get_type()) place_name = place.get_name().get_value() place_type = str(place.get_type()) place_id = place.get_gramps_id() @@ -145,6 +147,7 @@ def add_place(self, placeref, place, node, visited, drange): place_type, place_date, place_id, + place_hier, place_sort], node=node) diff --git a/gramps/plugins/gramplet/placedetails.py b/gramps/plugins/gramplet/placedetails.py index 9a03715bae3..d5446b8e1b7 100644 --- a/gramps/plugins/gramplet/placedetails.py +++ b/gramps/plugins/gramplet/placedetails.py @@ -65,7 +65,7 @@ def build_gui(self): self.top.show_all() return self.top - def add_row(self, title, value): + def add_row(self, title, value, val2=None): """ Add a row to the table. """ @@ -78,6 +78,10 @@ def add_row(self, title, value): value.show() self.grid.add(label) self.grid.attach_next_to(value, label, Gtk.PositionType.RIGHT, 1, 1) + if val2: + col2 = Gtk.Label(label=val2, halign=Gtk.Align.START) + col2.show() + self.grid.attach_next_to(col2, value, Gtk.PositionType.RIGHT, 1, 1) def clear_grid(self): """ @@ -90,10 +94,9 @@ def db_changed(self): self.connect_signal('Place', self.update) def update_has_data(self): - active_handle = self.get_active('Person') + active_handle = self.get_active('Place') if active_handle: - active_person = self.dbstate.db.get_person_from_handle(active_handle) - self.set_has_data(active_person is not None) + self.set_has_data(self.dbstate.db.has_place_handle(active_handle)) else: self.set_has_data(False) @@ -122,10 +125,22 @@ def display_place(self, place): markup_escape_text(title)) self.clear_grid() - self.add_row(_('Name'), place.get_name().get_value()) - self.add_row(_('Type'), place.get_type()) + names = [] + dates = [] + for name in place.get_names(): + names.append("%s (%s)" % (name.get_value(), name.get_language()) + if name.get_language() else name.get_value()) + dates.append("" if name.get_date_object().is_empty() else + "[%s]" % name.get_date_object()) + self.add_row(_('Names'), '\n'.join(names), val2='\n'.join(dates)) self.display_separator() - self.display_alt_names(place) + types = [] + dates = [] + for typ in place.get_types(): + types.append(typ.str(expand=True)) + dates.append("" if typ.get_date_object().is_empty() else + "[%s]" % typ.get_date_object()) + self.add_row(_('Types'), '\n'.join(types), val2='\n'.join(dates)) self.display_separator() lat, lon = conv_lat_lon(place.get_latitude(), place.get_longitude(), @@ -135,16 +150,6 @@ def display_place(self, place): if lon: self.add_row(_('Longitude'), lon) - def display_alt_names(self, place): - """ - Display alternative names for the place. - """ - alt_names = ["%s (%s)" % (name.get_value(), name.get_language()) - if name.get_language() else name.get_value() - for name in place.get_alternative_names()] - if len(alt_names) > 0: - self.add_row(_('Alternative Names'), '\n'.join(alt_names)) - def display_empty(self): """ Display empty details when no repository is selected. diff --git a/gramps/plugins/importer/importxml.py b/gramps/plugins/importer/importxml.py index 59663e54ba1..27638704057 100644 --- a/gramps/plugins/importer/importxml.py +++ b/gramps/plugins/importer/importxml.py @@ -53,9 +53,13 @@ Location, Media, MediaRef, Name, NameOriginType, NameType, Note, NoteType, Person, PersonRef, Place, PlaceName, PlaceRef, PlaceType, + PlaceAbbrev, PlaceAbbrevType, PlaceGroupType, + PlaceHierType, RepoRef, Repository, Researcher, Source, SrcAttribute, SrcAttributeType, StyledText, - StyledTextTag, StyledTextTagType, Surname, Tag, Url) + StyledTextTag, StyledTextTagType, Surname, Tag, + Url) +from gramps.gen.lib.placetype import DM_NAME from gramps.gen.db import DbTxn #from gramps.gen.db.write import CLASS_TO_KEY_MAP from gramps.gen.errors import GrampsImportError @@ -514,8 +518,8 @@ def __init__(self, database, user, change, default_tag_format=None): self.placeobj = None self.placeref = None self.place_name = None + self.place_type = None self.locations = 0 - self.place_names = 0 self.place_map = {} self.place_import = PlaceImport(self.db) @@ -663,6 +667,10 @@ def __init__(self, database, user, change, default_tag_format=None): "places": (None, self.stop_places), "placeobj": (self.start_placeobj, self.stop_placeobj), "placeref": (self.start_placeref, self.stop_placeref), + # new in 1.8.0 + "pabbr": (self.start_placeabbr, None), + "ptype": (self.start_place_type, self.stop_place_type), + "ptitle": (None, self.stop_ptitle), "pname": (self.start_place_name, self.stop_place_name), "locality": (None, self.stop_locality), @@ -732,24 +740,15 @@ class object of a primary object. :returns: The handle of the primary object. :rtype: str """ - handle = str(handle.replace('_', '')) + if handle.startswith('_'): + handle = handle[1:] orig_handle = handle if (orig_handle in self.import_handles and target in self.import_handles[orig_handle]): handle = self.import_handles[handle][target][HANDLE] if not isinstance(prim_obj, abc.Callable): # This method is called by a start_ method. - get_raw_obj_data = {"person": self.db.get_raw_person_data, - "family": self.db.get_raw_family_data, - "event": self.db.get_raw_event_data, - "place": self.db.get_raw_place_data, - "source": self.db.get_raw_source_data, - "citation": self.db.get_raw_citation_data, - "repository": self.db.get_raw_repository_data, - "media": self.db.get_raw_media_data, - "note": self.db.get_raw_note_data, - "tag": self.db.get_raw_tag_data}[target] - raw = get_raw_obj_data(handle) + raw = self.db.method("get_raw_%s_data", target)(handle) prim_obj.unserialize(raw) self.import_handles[orig_handle][target][INSTANTIATED] = True return handle @@ -767,16 +766,7 @@ class object of a primary object. while handle in self.import_handles: handle = create_id() else: - has_handle_func = {"person": self.db.has_person_handle, - "family": self.db.has_family_handle, - "event": self.db.has_event_handle, - "place": self.db.has_place_handle, - "source": self.db.has_source_handle, - "citation": self.db.get_raw_citation_data, - "repository": self.db.has_repository_handle, - "media": self.db.has_media_handle, - "note": self.db.has_note_handle, - "tag": self.db.has_tag_handle}[target] + has_handle_func = self.db.method("has_%s_handle", target) while has_handle_func(handle): handle = create_id() self.import_handles[orig_handle] = {target: [handle, False]} @@ -1137,14 +1127,16 @@ def start_placeobj(self, attrs): self.inaugurate_id(attrs.get('id'), PLACE_KEY, self.placeobj) self.placeobj.private = bool(attrs.get("priv")) self.placeobj.change = int(attrs.get('change', self.change)) + self.placeobj.group = PlaceGroupType(attrs.get("group")) # 1.8.0 if self.__xml_version == (1, 6, 0): place_name = PlaceName() place_name.set_value(attrs.get('name', '')) - self.placeobj.name = place_name - if 'type' in attrs: - self.placeobj.place_type.set_from_xml_str(attrs.get('type')) + self.placeobj.add_name(place_name) + if 'type' in attrs: # 1,7,x + ptype = PlaceType(attrs.get('type')) + self.placeobj.set_type(ptype) + self.placeobj.group = ptype.get_probable_group() self.info.add('new-object', PLACE_KEY, self.placeobj) - self.place_names = 0 # Gramps LEGACY: title in the placeobj tag self.placeobj.title = attrs.get('title', '') @@ -1180,23 +1172,42 @@ def start_location(self, attrs): attrs.get('county', ''), attrs.get('state', ''), attrs.get('country', '')) - self.place_import.store_location(location, self.placeobj.handle) + self.place_import.store_location(location, + self.placeobj.handle) - for level, name in enumerate(location): + for typ in ('street', 'locality', 'parish', 'city', 'county', + 'state', 'country'): + name = attrs.get(typ, '') if name: break + else: + typ = PlaceType.UNKNOWN place_name = PlaceName() place_name.set_value(name) self.placeobj.set_name(place_name) - type_num = 7 - level if name else PlaceType.UNKNOWN - self.placeobj.set_type(PlaceType(type_num)) - codes = [attrs.get('postal'), attrs.get('phone')] - self.placeobj.set_code(' '.join(code for code in codes if code)) + self.placeobj.set_type(PlaceType(typ)) + if attrs.get('postal'): + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(attrs.get('postal')) + self.placeobj.add_attribute(attr) + url = Url() + url.set_path(attrs.get('phone')) + url.set_type(_('Phone')) + self.placeobj.add_url(url) else: self.placeobj.add_alternate_locations(loc) self.locations = self.locations + 1 + def start_placeabbr(self, attrs): + """ + Add an abbreviation to the Place name + """ + abbr = PlaceAbbrev(value=attrs.get('value'), + type=PlaceAbbrevType(attrs.get('type', None))) + self.place_name.add_abbrev(abbr) + def start_witness(self, attrs): """ Add a note about a witness to the currently processed event or add @@ -1318,6 +1329,8 @@ def start_eventref(self, attrs): self.person.set_death_ref(self.eventref) else: self.person.add_event_ref(self.eventref) + elif self.placeobj: + self.placeobj.add_event_ref(self.eventref) def start_placeref(self, attrs): """ @@ -1326,7 +1339,11 @@ def start_placeref(self, attrs): self.placeref = PlaceRef() handle = self.inaugurate(attrs['hlink'], "place", Place) self.placeref.ref = handle - self.placeobj.add_placeref(self.placeref) + if 'type' in attrs: # Hierarchy type + self.placeref.type.set_from_xml_str(attrs["type"]) + else: # legacy most likely was an administrative hierarchy + self.placeref.type.set(PlaceHierType.ADMIN) + self.placeobj.add_placeref(self.placeref, sort=False) def start_attribute(self, attrs): self.attribute = Attribute() @@ -1349,6 +1366,8 @@ def start_attribute(self, attrs): self.person.add_attribute(self.attribute) elif self.family: self.family.add_attribute(self.attribute) + elif self.placeobj: + self.placeobj.add_attribute(self.attribute) def start_srcattribute(self, attrs): self.srcattribute = SrcAttribute() @@ -1691,19 +1710,62 @@ def start_parentin(self, attrs): def start_name(self, attrs): if self.person: self.start_person_name(attrs) - if self.placeobj: # XML 1.7.0 + elif self.placeobj: # XML 1.7.0 self.start_place_name(attrs) def start_place_name(self, attrs): self.place_name = PlaceName() self.place_name.set_value(attrs["value"]) if "lang" in attrs: - self.place_name.set_language(attrs["lang"]) - if self.place_names == 0: - self.placeobj.set_name(self.place_name) - else: - self.placeobj.add_alternative_name(self.place_name) - self.place_names += 1 + self.place_name.set_language(attrs["lang"]) + + def start_place_type(self, attrs): + """ added at 1.8.0 place type list with date, type, citation + """ + self.place_type = PlaceType() + # TODO this is temporary to allow import of last version of GEPS + leglist = [ + "Unknown", # -1 original value + "Country", # 1 + "State", # 2 + "County", # 3 + "City", # 4 + "Parish", # 5 + "Locality", # 6 + "Street", # 7 + "Province", # 8 + "Region", # 9 + "Department", # 10 + "Neighborhood", # 11 + "District", # 12 + "Borough", # 13 + "Municipality", # 14 + "Town", # 15 + "Village", # 16 + "Hamlet", # 17 + "Farm", # 18 + "Building", # 19 + "Number"] # 20 + if "value" in attrs: + if "number" in attrs: + if int(attrs["number"]) < 0: + self.place_type.pt_id = "GOV_%d" % -int(attrs["number"]) + self.place_type.name = attrs["value"] + return + try: + ptn = leglist[int(attrs["number"])] + except IndexError: + ptn = PlaceType.CUSTOM + self.place_type.pt_id = ptn + self.place_type.name = attrs["value"] + return + self.place_type.pt_id = PlaceType.CUSTOM + self.place_type.name = attrs["value"] + return + # TODO end of block that allows older version of GEPS import + # newest version of GEPS just has pt_id as the attribute + self.place_type.pt_id = attrs["pt_id"] + self.place_type.name = attrs["name"] def start_person_name(self, attrs): if not self.in_witness: @@ -2047,6 +2109,12 @@ def __add_citation(self, citation_handle): self.address.add_citation(citation_handle) elif self.name: self.name.add_citation(citation_handle) + elif self.placeref: + self.placeref.add_citation(citation_handle) + elif self.place_name: + self.place_name.add_citation(citation_handle) + elif self.place_type: + self.place_type.add_citation(citation_handle) elif self.placeobj: self.placeobj.add_citation(citation_handle) elif self.childref: @@ -2350,6 +2418,8 @@ def start_compound_date(self, attrs, mode): date_value = self.placeref.get_date_object() elif self.place_name: date_value = self.place_name.get_date_object() + elif self.place_type: + date_value = self.place_type.get_date_object() start = attrs['start'].split('-') stop = attrs['stop'].split('-') @@ -2442,6 +2512,8 @@ def start_dateval(self, attrs): date_value = self.placeref.get_date_object() elif self.place_name: date_value = self.place_name.get_date_object() + elif self.place_type: + date_value = self.place_type.get_date_object() bce = 1 val = attrs['val'] @@ -2544,6 +2616,8 @@ def start_datestr(self, attrs): date_value = self.event.get_date_object() elif self.placeref: date_value = self.placeref.get_date_object() + elif self.place_type: + date_value = self.place_type.get_date_object() else: date_value = self.place_name.get_date_object() @@ -2613,16 +2687,19 @@ def stop_ptitle(self, tag): self.placeobj.title = tag def stop_code(self, tag): - self.placeobj.code = tag + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(tag) + self.placeobj.add_attribute(attr) def stop_alt_name(self, tag): place_name = PlaceName() place_name.set_value(tag) - self.placeobj.add_alternative_name(place_name) + self.placeobj.add_name(place_name, sort=False) def stop_placeobj(self, *tag): - if self.placeobj.name.get_value() == '': - self.placeobj.name.set_value(self.placeobj.title) + if not self.placeobj.get_names: + self.placeobj.add_name(PlaceName(value=self.placeobj.title)) self.db.commit_place(self.placeobj, self.trans, self.placeobj.get_change_time()) self.placeobj = None @@ -2699,9 +2776,15 @@ def stop_name(self, attrs): if self.placeobj: # XML 1.7.0 self.stop_place_name(attrs) - def stop_place_name(self, tag): + def stop_place_name(self, _name): + self.placeobj.add_name(self.place_name, sort=False) self.place_name = None + def stop_place_type(self, _ptype): + """ new 1.8.0 """ + self.placeobj.add_type(self.place_type, sort=False) + self.place_type = None + def stop_person_name(self, tag): if self.in_witness: # Parse witnesses created by older gramps diff --git a/gramps/plugins/lib/libgrampsxml.py b/gramps/plugins/lib/libgrampsxml.py index 3e9b04e0848..16623b8d1aa 100644 --- a/gramps/plugins/lib/libgrampsxml.py +++ b/gramps/plugins/lib/libgrampsxml.py @@ -33,5 +33,5 @@ # Public Constants # #------------------------------------------------------------------------ -GRAMPS_XML_VERSION_TUPLE = (1, 7, 1) # version for Gramps 4.2 +GRAMPS_XML_VERSION_TUPLE = (1, 8, 0) # version for Gramps 5.1 GRAMPS_XML_VERSION = '.'.join(str(i) for i in GRAMPS_XML_VERSION_TUPLE) diff --git a/gramps/plugins/lib/libplaceimport.py b/gramps/plugins/lib/libplaceimport.py index 82689ca3b2c..1c2e6e251d4 100644 --- a/gramps/plugins/lib/libplaceimport.py +++ b/gramps/plugins/lib/libplaceimport.py @@ -20,6 +20,9 @@ """ Helper class for importing places. +Note: this is used for importing old Location based places and converting to +the more recent enclosed places (Gramps 4.2.x) and is not likely useful for +new work. """ from collections import OrderedDict @@ -97,10 +100,16 @@ def generate_hierarchy(self, trans): # link to existing place if parent: place = self.db.get_place_from_handle(handle) - placeref = PlaceRef() - placeref.ref = parent - place.set_placeref_list([placeref]) - self.db.commit_place(place, trans, place.get_change_time()) + if not place.get_placeref_list(): # Only if not enclosed + placeref = PlaceRef() + placeref.ref = parent + placeref.set_type_for_place( + self.db.get_place_from_handle(parent)) + place.set_placeref_list([placeref]) + self.db.commit_place(place, trans, place.get_change_time()) + + locs = ['street', 'locality', 'parish', 'city', + 'county', 'state', 'country'] def __add_place(self, name, type_num, parent, title, trans): """ @@ -109,12 +118,16 @@ def __add_place(self, name, type_num, parent, title, trans): place = Place() place_name = PlaceName() place_name.set_value(name) - place.name = place_name + place.set_name(place_name) place.title = title - place.place_type = PlaceType(7-type_num) + ptype = PlaceType(self.locs[type_num]) + place.set_type(ptype) + place.set_group(ptype.get_probable_group()) if parent is not None: placeref = PlaceRef() placeref.ref = parent + placeref.set_type_for_place( + self.db.get_place_from_handle(parent)) place.set_placeref_list([placeref]) handle = self.db.add_place(place, trans) self.db.commit_place(place, trans) diff --git a/gramps/plugins/lib/libplaceview.py b/gramps/plugins/lib/libplaceview.py index 4bbb309b4fc..b54f552ec08 100644 --- a/gramps/plugins/lib/libplaceview.py +++ b/gramps/plugins/lib/libplaceview.py @@ -73,12 +73,12 @@ class PlaceBaseView(ListView): COL_ID = 1 COL_TITLE = 2 COL_TYPE = 3 - COL_CODE = 4 - COL_LAT = 5 - COL_LON = 6 - COL_PRIV = 7 - COL_TAGS = 8 - COL_CHAN = 9 + COL_LAT = 4 + COL_LON = 5 + COL_PRIV = 6 + COL_TAGS = 7 + COL_CHAN = 8 + COL_GROUP = 9 COL_SEARCH = 11 # column definitions COLUMNS = [ @@ -86,19 +86,19 @@ class PlaceBaseView(ListView): (_('ID'), TEXT, None), (_('Title'), TEXT, None), (_('Type'), TEXT, None), - (_('Code'), TEXT, None), (_('Latitude'), TEXT, None), (_('Longitude'), TEXT, None), (_('Private'), ICON, 'gramps-lock'), (_('Tags'), TEXT, None), (_('Last Changed'), TEXT, None), - ] + (_('Group'), TEXT, None), ] + # default setting with visible columns, order of the col, and their size CONFIGSETTINGS = ( - ('columns.visible', [COL_NAME, COL_ID, COL_TYPE, COL_CODE]), - ('columns.rank', [COL_NAME, COL_TITLE, COL_ID, COL_TYPE, COL_CODE, + ('columns.visible', [COL_NAME, COL_ID, COL_TYPE]), + ('columns.rank', [COL_NAME, COL_TITLE, COL_ID, COL_TYPE, COL_GROUP, COL_LAT, COL_LON, COL_PRIV, COL_TAGS, COL_CHAN]), - ('columns.size', [250, 250, 75, 100, 100, 150, 150, 40, 100, 100]) + ('columns.size', [250, 250, 75, 100, 75, 150, 150, 40, 100, 100]) ) ADD_MSG = _("Add a new place") EDIT_MSG = _("Edit the selected place") @@ -611,6 +611,7 @@ def get_default_gramplets(self): ("Place Details", "Place Enclosed By", "Place Encloses", + "Place Events", "Place Gallery", "Place Citations", "Place Notes", diff --git a/gramps/plugins/lib/maps/geography.py b/gramps/plugins/lib/maps/geography.py index ea732cd2401..6d7d55caf73 100644 --- a/gramps/plugins/lib/maps/geography.py +++ b/gramps/plugins/lib/maps/geography.py @@ -1013,7 +1013,7 @@ def add_place_from_kml(self, menu, event, lat, lon): place_name = PlaceName() place_name.set_value(name) new_place = Place() - new_place.set_name(place_name) + new_place.add_name(place_name) new_place.set_title(name) new_place.set_latitude(str(lat)) new_place.set_longitude(str(lon)) @@ -1026,15 +1026,14 @@ def add_place_from_kml(self, menu, event, lat, lon): def place_exists(self, place_name): """ Do we have already this place in our database ? - return the handle for this place. + return the place. """ - found = None place_name = place_name.replace('-', ' ').lower() for place in self.dbstate.db.iter_places(): - if place.name.get_value().lower() == place_name: - found = place.handle - break - return found + for pname in place.get_names(): + if pname.get_value().lower() == place_name: + return place + return None def link_place(self, menu, event, lat, lon): """ @@ -1107,7 +1106,8 @@ def __add_place(self, parent, plat, plon): if parent: if isinstance(parent, Place): placeref = PlaceRef() - placeref.ref = parent + placeref.set_type_for_place(parent) + placeref.ref = parent.handle new_place.add_placeref(placeref) elif isinstance(parent, gi.overrides.Gtk.TreeModelRow): # We are here because we selected a place from geocoding @@ -1118,24 +1118,29 @@ def __add_place(self, parent, plat, plon): value = PlaceSelection.untag_text(parent[2], 1) plname = PlaceName() plname.set_value(value) - handle = self.place_exists(value) - if handle: + plc = self.place_exists(value) + if plc: # The town already exists. We create a place with name placeref = PlaceRef() - placeref.ref = handle + placeref.ref = plc.handle + placeref.set_type_for_place(plc) new_place.add_placeref(placeref) value = PlaceSelection.untag_text(parent[3], 1) plname.set_value(value) - new_place.set_name(plname) + new_place.add_name(plname) else: found = None for place in self.dbstate.db.iter_places(): - found = place - if place.name.get_value() == parent: + for pname in place.get_names(): + if pname.get_value() == parent: + found = place + break + if found: + placeref = PlaceRef() + placeref.ref = found.get_handle() + placeref.set_type_for_place(found) + new_place.add_placeref(placeref) break - placeref = PlaceRef() - placeref.ref = found.get_handle() - new_place.add_placeref(placeref) try: EditPlace(self.dbstate, self.uistate, [], new_place) self.add_marker(None, None, plat, plon, None, True, 0) @@ -1160,6 +1165,7 @@ def __link_place(self, parent, plat, plon): """ Link an existing place using longitude and latitude of location centered on the map + TODO Does this ever get called? """ selector = SelectPlace(self.dbstate, self.uistate, []) place = selector.run() @@ -1170,6 +1176,8 @@ def __link_place(self, parent, plat, plon): if parent: placeref = PlaceRef() placeref.ref = parent + placeref.set_type_for_place( + self.dbstate.db.get_place_from_handle(parent)) place.add_placeref(placeref) try: EditPlace(self.dbstate, self.uistate, [], place) diff --git a/gramps/plugins/lib/maps/markerlayer.py b/gramps/plugins/lib/maps/markerlayer.py index a097ab94b5a..6dd4d3be1e7 100644 --- a/gramps/plugins/lib/maps/markerlayer.py +++ b/gramps/plugins/lib/maps/markerlayer.py @@ -157,7 +157,7 @@ def do_draw(self, gpsmap, ctx): else: # We use colored icons. draw_marker(ctx, float(coord_x), float(coord_y), - size, marker[3][1]) + size, marker[3]) _LOG.debug("%s", time.strftime("end drawing : " "%a %d %b %Y %H:%M:%S", time.gmtime())) diff --git a/gramps/plugins/lib/maps/placeselection.py b/gramps/plugins/lib/maps/placeselection.py index e21f7d8d9aa..15c7723ee69 100644 --- a/gramps/plugins/lib/maps/placeselection.py +++ b/gramps/plugins/lib/maps/placeselection.py @@ -62,7 +62,7 @@ from gramps.gui.dialog import WarningDialog from .osmgps import OsmGps from gramps.gen.utils.location import get_main_location -from gramps.gen.lib import PlaceType +from gramps.gen.lib import PlaceGroupType as P_G from gramps.gen.utils.place import conv_lat_lon from gramps.gen.display.place import displayer as _pd @@ -197,9 +197,9 @@ def slider_change(self, obj, lat, lon): self.label2.show() place = self.dbstate.db.get_place_from_handle(self.oldvalue) loc = get_main_location(self.dbstate.db, place) - self.plist.append((PLACE_STRING % loc.get(PlaceType.COUNTRY, ''), - PLACE_STRING % loc.get(PlaceType.STATE, ''), - PLACE_STRING % loc.get(PlaceType.COUNTY, ''), + self.plist.append((PLACE_STRING % loc.get("Country", ''), + PLACE_STRING % loc.get("State", ''), + PLACE_STRING % loc.get("County", ''), PLACE_STRING % _('Other'), self.oldvalue) ) @@ -247,25 +247,26 @@ def get_location(self, gramps_id): parent_place = None country = state = county = other = '' place = self.dbstate.db.get_place_from_gramps_id(gramps_id) - place_name = place.name.get_value() + place_name = place.get_names()[0].get_value() parent_list = place.get_placeref_list() while len(parent_list) > 0: place = self.dbstate.db.get_place_from_handle(parent_list[0].ref) parent_list = place.get_placeref_list() - if int(place.get_type()) == PlaceType.COUNTY: - county = place.name.get_value() + if(place.get_type() == "County" or + place.group == P_G.REGION and not county): # County + county = place.get_names()[0].get_value() if parent_place is None: parent_place = place.get_handle() - elif int(place.get_type()) == PlaceType.STATE: - state = place.name.get_value() + elif place.group == P_G.REGION: # Terrritory, State + state = place.get_names()[0].get_value() if parent_place is None: parent_place = place.get_handle() - elif int(place.get_type()) == PlaceType.COUNTRY: - country = place.name.get_value() + elif place.group == P_G.COUNTRY: # Countries + country = place.get_names()[0].get_value() if parent_place is None: parent_place = place.get_handle() else: - other = place.name.get_value() + other = place.get_names()[0].get_value() if parent_place is None: parent_place = place.get_handle() return(country, state, county, place_name, other) diff --git a/gramps/plugins/mapservices/eniroswedenmap.py b/gramps/plugins/mapservices/eniroswedenmap.py index deff0e97394..0301791c3a3 100644 --- a/gramps/plugins/mapservices/eniroswedenmap.py +++ b/gramps/plugins/mapservices/eniroswedenmap.py @@ -43,7 +43,7 @@ from gramps.gen.display.place import displayer as place_displayer from gramps.gen.lib import PlaceType -# Make upper case of translated country so string search works later +# Make upper case of translaed country so string search works later MAP_NAMES_SWEDEN = [_("Sweden").upper(), "SVERIGE", "SWEDEN", @@ -70,9 +70,9 @@ def _build_title(db, place): """ Builds descrition string for title parameter in url """ descr = place_displayer.display(db, place) location = get_main_location(db, place) - parish = location.get(PlaceType.PARISH) - city = location.get(PlaceType.CITY) - state = location.get(PlaceType.STATE) + parish = location.get("Parish") + city = location.get("City") + state = location.get("State") title_descr = "" if descr: title_descr += descr.strip() @@ -90,7 +90,7 @@ def _build_title(db, place): def _build_city(db, place): """ Builds description string for city parameter in url """ location = get_main_location(db, place) - county = location.get(PlaceType.COUNTY) + county = location.get("County") # Build a title description string that will work for Eniro city_descr = _build_area(db, place) if county: @@ -101,8 +101,8 @@ def _build_city(db, place): def _build_area(db, place): """ Builds string for area parameter in url """ location = get_main_location(db, place) - street = location.get(PlaceType.STREET) - city = location.get(PlaceType.CITY) + street = location.get("Street") + city = location.get("City") # Build a title description string that will work for Eniro area_descr = "" if street: @@ -131,7 +131,7 @@ def calc_url(self): # First see if we are in or near Sweden or Denmark # Change country to upper case location = get_main_location(self.database, place) - country = location.get(PlaceType.COUNTRY, '').upper().strip() + country = location.get("Country", '').upper().strip() country_given = (country in MAP_NAMES_SWEDEN or \ country in MAP_NAMES_DENMARK) and (country != "") # if no country given, check if we might be in the vicinity defined by diff --git a/gramps/plugins/textreport/placereport.py b/gramps/plugins/textreport/placereport.py index f2139d142b5..7cec0e9bfca 100644 --- a/gramps/plugins/textreport/placereport.py +++ b/gramps/plugins/textreport/placereport.py @@ -187,7 +187,7 @@ def __write_place(self, handle, place_nbr): 'str2': level[0]}) place_names = '' - all_names = place.get_all_names() + all_names = place.get_names() if len(all_names) > 1 or __debug__: for place_name in all_names: if place_names != '': diff --git a/gramps/plugins/tool/check.py b/gramps/plugins/tool/check.py index 16e1591791a..e4ec7415621 100644 --- a/gramps/plugins/tool/check.py +++ b/gramps/plugins/tool/check.py @@ -59,8 +59,8 @@ _ = glocale.translation.gettext ngettext = glocale.translation.ngettext # else "nearby" comments are ignored from gramps.gen.lib import (Citation, Event, EventType, Family, Media, - Name, Note, Person, Place, Repository, Source, - StyledText, StyledTextTagType, Tag) + Name, Note, Person, Place, PlaceName, Repository, + Source, StyledText, StyledTextTagType, Tag) from gramps.gen.db import DbTxn, CLASS_TO_KEY_MAP from gramps.gen.config import config from gramps.gen.utils.id import create_id @@ -193,7 +193,7 @@ def __init__(self, dbstate, user, options_class, name, callback=None): # then going to be deleted. checker.cleanup_empty_objects() checker.fix_encoding() - checker.fix_alt_place_names() + checker.fix_place_names() checker.fix_ctrlchars_in_notes() checker.cleanup_missing_photos(cli) checker.cleanup_deleted_name_formats() @@ -454,35 +454,36 @@ def fix_ctrlchars_in_notes(self): if error_count == 0: logging.info(' OK: no ctrl characters in notes found') - def fix_alt_place_names(self): + def fix_place_names(self): """ This scans all places and cleans up alternative names. It removes Blank names, names that are duplicates of the primary name, and duplicates in the alt_names list. """ - self.progress.set_pass(_('Looking for bad alternate place names'), + self.progress.set_pass(_('Looking for bad place names'), self.db.get_number_of_places()) - logging.info('Looking for bad alternate place names') + logging.info('Looking for bad place names') for handle in self.db.get_place_handles(): place = self.db.get_place_from_handle(handle) - fixed_alt_names = [] + fixed_names = [] fixup = False - for name in place.get_alternative_names(): + for name in place.get_names(): if not name.value or \ - name == place.name or \ - name in fixed_alt_names: + name in fixed_names: fixup = True continue - fixed_alt_names.append(name) + fixed_names.append(name) if fixup: - place.set_alternative_names(fixed_alt_names) + if not fixed_names: + fixed_names.append(PlaceName(value=_("Unknown"))) + place.set_names(fixed_names) self.db.commit_place(place, self.trans) self.place_errors += 1 self.progress.step() if self.place_errors == 0: - logging.info(' OK: no bad alternate places found') + logging.info(' OK: no bad place names found') else: - logging.info(' %d bad alternate places found and fixed', + logging.info(' %d bad place names found and fixed', self.place_errors) def check_for_broken_family_links(self): diff --git a/gramps/plugins/view/eventview.py b/gramps/plugins/view/eventview.py index bb9aea8eebd..2a5cf9ac52f 100644 --- a/gramps/plugins/view/eventview.py +++ b/gramps/plugins/view/eventview.py @@ -412,13 +412,18 @@ def delete_event_response(self, event): """ Delete the event from the database. """ - person_list = [item[1] for item in + person_list = [ + item[1] for item in self.dbstate.db.find_backlink_handles(event.handle, ['Person'])] - family_list = [item[1] for item in + family_list = [ + item[1] for item in self.dbstate.db.find_backlink_handles(event.handle, ['Family'])] + place_list = [ + item[1] for item in + self.dbstate.db.find_backlink_handles(event.handle, ['Place'])] query = DeleteEventQuery(self.dbstate, self.uistate, event, - person_list, family_list) + person_list, family_list, place_list) query.query_response() def delete_multi_event_response(self, handles=None): @@ -454,6 +459,14 @@ def delete_multi_event_response(self, handles=None): family.remove_handle_references('Event', ev_handle_list) _db.commit_family(family, trans) + place_list = [ + item[1] for item in + _db.find_backlink_handles(handle, ['Place'])] + for hndl in place_list: + place = _db.get_place_from_handle(hndl) + place.remove_handle_references('Event', ev_handle_list) + _db.commit_place(place, trans) + _db.remove_event(handle, trans) self.uistate.pulse_progressbar(indx / hndl_cnt) trans.set_description(_("Multiple Selection Delete")) diff --git a/gramps/plugins/view/geoplaces.py b/gramps/plugins/view/geoplaces.py index bc520c7ff22..9d43d211408 100644 --- a/gramps/plugins/view/geoplaces.py +++ b/gramps/plugins/view/geoplaces.py @@ -51,7 +51,7 @@ from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext from gramps.gen.lib import EventType -from gramps.gen.lib import PlaceType +from gramps.gen.lib.placetype import PlaceType, DM_NAME from gramps.gen.config import config from gramps.gen.display.place import displayer as _pd from gramps.gen.utils.place import conv_lat_lon @@ -177,31 +177,6 @@ class GeoPlaces(GeoGraphyView): ('geography.max_places', 5000), ('geography.use-keypad', True), ('geography.personal-map', ""), - - # specific to geoplaces : - - ('geography.color.unknown', '#008b00'), - ('geography.color.custom', '#008b00'), - ('geography.color.country', '#008b00'), - ('geography.color.county', '#008b00'), - ('geography.color.state', '#008b00'), - ('geography.color.city', '#008b00'), - ('geography.color.parish', '#008b00'), - ('geography.color.locality', '#008b00'), - ('geography.color.street', '#008b00'), - ('geography.color.province', '#008b00'), - ('geography.color.region', '#008b00'), - ('geography.color.department', '#008b00'), - ('geography.color.neighborhood', '#008b00'), - ('geography.color.district', '#008b00'), - ('geography.color.borough', '#008b00'), - ('geography.color.municipality', '#008b00'), - ('geography.color.town', '#008b00'), - ('geography.color.village', '#008b00'), - ('geography.color.hamlet', '#008b00'), - ('geography.color.farm', '#008b00'), - ('geography.color.building', '#008b00'), - ('geography.color.number', '#008b00'), ) def __init__(self, pdata, dbstate, uistate, nav_group=0): @@ -227,8 +202,6 @@ def __init__(self, pdata, dbstate, uistate, nav_group=0): self.itemoption = None self.menu = None self.cal = config.get('preferences.calendar-format-report') - self.plc_color = [] - self.plc_custom_color = defaultdict(set) def get_title(self): """ @@ -309,13 +282,7 @@ def _create_one_place(self, place): # one string. We have coordinates when the two values # contains non null string. if longitude and latitude: - colour = self.plc_color[int(place.get_type())+1] - if int(place.get_type()) == PlaceType.CUSTOM: - try: - colour = (str(place.get_type()), - self.plc_custom_color[str(place.get_type())]) - except: - colour = self.plc_color[PlaceType.CUSTOM + 1] + colour = place.get_type().get_color() self._append_to_places_list(descr, None, "", latitude, longitude, None, None, @@ -353,30 +320,6 @@ def _createmap(self, place_x): self.kml_layer.clear() self.no_show_places_in_status_bar = False _col = self._config.get - self.plc_color = [ - (PlaceType.UNKNOWN, _col('geography.color.unknown')), - (PlaceType.CUSTOM, _col('geography.color.custom')), - (PlaceType.COUNTRY, _col('geography.color.country')), - (PlaceType.STATE, _col('geography.color.state')), - (PlaceType.COUNTY, _col('geography.color.county')), - (PlaceType.CITY, _col('geography.color.city')), - (PlaceType.PARISH, _col('geography.color.parish')), - (PlaceType.LOCALITY, _col('geography.color.locality')), - (PlaceType.STREET, _col('geography.color.street')), - (PlaceType.PROVINCE, _col('geography.color.province')), - (PlaceType.REGION, _col('geography.color.region')), - (PlaceType.DEPARTMENT, _col('geography.color.department')), - (PlaceType.NEIGHBORHOOD, _col('geography.color.neighborhood')), - (PlaceType.DISTRICT, _col('geography.color.district')), - (PlaceType.BOROUGH, _col('geography.color.borough')), - (PlaceType.MUNICIPALITY, _col('geography.color.municipality')), - (PlaceType.TOWN, _col('geography.color.town')), - (PlaceType.VILLAGE, _col('geography.color.village')), - (PlaceType.HAMLET, _col('geography.color.hamlet')), - (PlaceType.FARM, _col('geography.color.farm')), - (PlaceType.BUILDING, _col('geography.color.building')), - (PlaceType.NUMBER, _col('geography.color.number')) - ] # base "villes de france" : 38101 places : # createmap : 8'50"; create_markers : 1'23" # base "villes de france" : 38101 places : @@ -394,7 +337,6 @@ def _createmap(self, place_x): # create_markers: 0'01"; draw markers: 0'07" _LOG.debug("%s", time.strftime("start createmap : " "%a %d %b %Y %H:%M:%S", time.gmtime())) - self.custom_places() if self.show_all: self.show_all = False try: @@ -575,108 +517,3 @@ def get_default_gramplets(self): """ return (("Place Filter",), ()) - - def specific_options(self, configdialog): - """ - Add specific entry to the preference menu. - Must be done in the associated view. - """ - grid = Gtk.Grid() - grid.set_border_width(12) - grid.set_column_spacing(6) - grid.set_row_spacing(6) - configdialog.add_color(grid, - _("Unknown"), - 1, 'geography.color.unknown', col=1) - configdialog.add_color(grid, - _("Custom"), - 2, 'geography.color.custom', col=1) - configdialog.add_color(grid, - _("Locality"), - 3, 'geography.color.locality', col=1) - configdialog.add_color(grid, - _("Street"), - 4, 'geography.color.street', col=1) - configdialog.add_color(grid, - _("Neighborhood"), - 5, 'geography.color.neighborhood', col=1) - configdialog.add_color(grid, - _("Borough"), - 6, 'geography.color.borough', col=1) - configdialog.add_color(grid, - _("Village"), - 7, 'geography.color.village', col=1) - configdialog.add_color(grid, - _("Hamlet"), - 8, 'geography.color.hamlet', col=1) - configdialog.add_color(grid, - _("Farm"), - 9, 'geography.color.farm', col=1) - configdialog.add_color(grid, - _("Building"), - 10, 'geography.color.building', col=1) - configdialog.add_color(grid, - _("Number"), - 11, 'geography.color.number', col=1) - configdialog.add_color(grid, - _("Country"), - 1, 'geography.color.country', col=4) - configdialog.add_color(grid, - _("State"), - 2, 'geography.color.state', col=4) - configdialog.add_color(grid, - _("County"), - 3, 'geography.color.county', col=4) - configdialog.add_color(grid, - _("Province"), - 4, 'geography.color.province', col=4) - configdialog.add_color(grid, - _("Region"), - 5, 'geography.color.region', col=4) - configdialog.add_color(grid, - _("Department"), - 6, 'geography.color.department', col=4) - configdialog.add_color(grid, - _("District"), - 7, 'geography.color.district', col=4) - configdialog.add_color(grid, - _("Parish"), - 8, 'geography.color.parish', col=4) - configdialog.add_color(grid, - _("City"), - 9, 'geography.color.city', col=4) - configdialog.add_color(grid, - _("Town"), - 10, 'geography.color.town', col=4) - configdialog.add_color(grid, - _("Municipality"), - 11, 'geography.color.municipality', col=4) - self.custom_places() - if len(self.plc_custom_color) > 0: - configdialog.add_text(grid, _("Custom places name"), 12) - start = 13 - for color in self.plc_custom_color.keys(): - cust_col = 'geography.color.' + color.lower() - row = start if start % 2 else start -1 - column = 1 if start %2 else 4 - configdialog.add_color(grid, color, - row, cust_col, col=column) - start += 1 - return _('The places marker color'), grid - - def custom_places(self): - """ - looking for custom places - if not registered, register it. - """ - self.plc_custom_color = defaultdict(set) - for place in self.dbstate.db.iter_places(): - if int(place.get_type()) == PlaceType.CUSTOM: - cust_col = 'geography.color.' + str(place.get_type()).lower() - try: - color = self._config.get(cust_col) - except: - color = '#008b00' - self._config.register(cust_col, color) - if str(place.get_type()) not in self.plc_custom_color.keys(): - self.plc_custom_color[str(place.get_type())] = color.lower() diff --git a/gramps/plugins/view/placetreeview.py b/gramps/plugins/view/placetreeview.py index c6c650394b5..3ceecbe4cc0 100644 --- a/gramps/plugins/view/placetreeview.py +++ b/gramps/plugins/view/placetreeview.py @@ -107,6 +107,8 @@ def add(self, *obj): for handle in self.selected_handles(): placeref = PlaceRef() placeref.ref = handle + placeref.set_type_for_place( + self.dbstate.db.get_place_from_handle(handle)) parent_list.append(placeref) place = Place() From 06516faf086e186ac34f3d1dd915d144bc963f4f Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 09:58:49 -0500 Subject: [PATCH 02/26] GEPS045 Place Formats --- gramps/gen/config.py | 2 - gramps/gen/const.py | 2 +- gramps/gen/display/place.py | 441 +++++++++++++------ gramps/gen/utils/location.py | 155 +++++-- gramps/gui/configure.py | 13 +- gramps/gui/editors/editplaceformat.py | 419 +++++++++++++++--- gramps/gui/glade/editplaceformat.glade | 561 +++++++++++++++++++++++-- 7 files changed, 1332 insertions(+), 261 deletions(-) diff --git a/gramps/gen/config.py b/gramps/gen/config.py index f2426815ca4..12a8835f069 100644 --- a/gramps/gen/config.py +++ b/gramps/gen/config.py @@ -214,8 +214,6 @@ def emit(key): register('interface.pedview-tree-size', 5) register('interface.pedview-tree-direction', 2) register('interface.pedview-show-unknown-people', False) -register('interface.place-name-height', 100) -register('interface.place-name-width', 450) register('interface.sidebar-text', True) register('interface.size-checked', False) register('interface.statusbar', 1) diff --git a/gramps/gen/const.py b/gramps/gen/const.py index aff4a22d13b..d4d23b9fd19 100644 --- a/gramps/gen/const.py +++ b/gramps/gen/const.py @@ -119,7 +119,7 @@ CUSTOM_FILTERS = os.path.join(VERSION_DIR, "custom_filters.xml") REPORT_OPTIONS = os.path.join(HOME_DIR, "report_options.xml") TOOL_OPTIONS = os.path.join(HOME_DIR, "tool_options.xml") -PLACE_FORMATS = os.path.join(HOME_DIR, "place_formats.xml") +PLACE_FORMATS = os.path.join(HOME_DIR, "place_formats.json") ENV_DIR = os.path.join(HOME_DIR, "env") TEMP_DIR = os.path.join(HOME_DIR, "temp") diff --git a/gramps/gen/display/place.py b/gramps/gen/display/place.py index 5bb1c204c54..4c4531475aa 100644 --- a/gramps/gen/display/place.py +++ b/gramps/gen/display/place.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2014-2017 Nick Hall +# Copyright (C) 2019 Paul Culley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,18 +29,50 @@ # #--------------------------------------------------------------- import os -import xml.dom.minidom - +import json #------------------------------------------------------------------------- # # Gramps modules # #------------------------------------------------------------------------- -from ..const import PLACE_FORMATS, GRAMPS_LOCALE as glocale -_ = glocale.translation.gettext +import gramps.gen.lib as lib from ..config import config from ..utils.location import get_location_list -from ..lib import PlaceType +from ..lib import PlaceGroupType as P_G +from ..lib import PlaceHierType +from ..const import PLACE_FORMATS +from ..const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext + + +def _default(obj): + """ json function to manage saving objects in our PlaceFormats """ + obj_dict = {'_class': obj.__class__.__name__} + if isinstance(obj, lib.GrampsType): + obj_dict['string'] = getattr(obj, 'string') + else: + obj_dict.update(obj.__dict__.items()) + return obj_dict + + +def _object_hook(obj_dict): + """ json function to manage loading objects for our PlaceFormats """ + cls = obj_dict['_class'] + if cls == 'PlaceFormat': + obj = PlaceFormat.__new__(PlaceFormat) + elif cls == 'PlaceRule': + obj = PlaceRule.__new__(PlaceRule) + else: + obj = getattr(lib, cls)() + if isinstance(obj, lib.GrampsType): + setattr(obj, 'string', obj_dict['string']) + else: + obj.__dict__.update(obj_dict) + return obj + + +PFVERS = 1 # Place Format Version, change if incompatible with older + #------------------------------------------------------------------------- # @@ -47,12 +80,59 @@ # #------------------------------------------------------------------------- class PlaceFormat: - def __init__(self, name, levels, language, street, reverse): - self.name = name - self.levels = levels - self.language = language - self.street = street - self.reverse = reverse + """ + This class stores the basic information about the place format + """ + def __init__(self, name, language='', reverse=False, rules=None): + self.name = name # str name of format + self.language = language # Language desired of format + self.reverse = reverse # Order of place title names is reversed + if rules is None: + rules = [] + self.rules = rules # list of rules for the format + self.version = PFVERS # detects if the format or rule is different + + +#------------------------------------------------------------------------- +# +# PlaceRule class +# +#------------------------------------------------------------------------- +class PlaceRule: + """ + This class stores the place format rules. + """ + V_NONE = 0 # visibility of item; None + V_STNUM = 1 # visibility of street/number; visible, street first + V_NUMST = 2 # visibility of street/number; visible, number first + V_ALL = 3 # visibility of item; All + V_SMALL = 4 # visibility of item; Only smallest of group or type + V_LARGE = 5 # visibility of item; Only largest of group or type + T_GRP = 0 # What does rule work with; Place Group + T_TYP = 1 # What does rule work with; Place Type + T_ST_NUM = 2 # What does rule work with; Street and number + A_NONE = -2 # indicates no abbrev, val is added to PlaceAbbrevType + A_FIRST = -3 # indicates first abbrev, val is added to PlaceAbbrevType + + VIS_MAP = { + V_NONE: _('Hidden'), + V_SMALL: _("Show Smallest"), + V_LARGE: _("Show Largest"), + V_ALL: _("Show All"), + V_STNUM: _("Street Number"), + V_NUMST: _("Number Street") + } + + def __init__(self, where, r_type, r_value, vis, abb, hier): + """ Place Format Rule """ + self.hier = hier # PlaceHierType of format + self.where = where # None, or place handle + self.where_id = None # the place gramps_id + self.where_title = None # the place title + self.type = r_type # on of T_ group, type, or street/num + self.value = r_value # int, PlaceType group or type number + self.vis = vis # int, one of the V_ values above + self.abb = abb # PlaceAbbrevType with extra values, A_NONE, A_FIRST #------------------------------------------------------------------------- @@ -61,142 +141,261 @@ def __init__(self, name, levels, language, street, reverse): # #------------------------------------------------------------------------- class PlaceDisplay: - + """ + This is the place title display and format storage class. + """ def __init__(self): self.place_formats = [] - self.default_format = config.get('preferences.place-format') - if os.path.exists(PLACE_FORMATS): - try: - self.load_formats() - return - except BaseException: - print(_("Error in '%s' file: cannot load.") % PLACE_FORMATS) - pf = PlaceFormat(_('Full'), ':', '', 0, False) - self.place_formats.append(pf) + # initialize the default format + _pf = PlaceFormat(_('Full')) + self.place_formats.append(_pf) - def display_event(self, db, event, fmt=-1): + def display_event(self, _db, event, fmt=-1): + """ + This displays an event's place title according to the specified + format. + """ if not event: return "" place_handle = event.get_place_handle() if place_handle: - place = db.get_place_from_handle(place_handle) - return self.display(db, place, event.get_date_object(), fmt) - else: - return "" + place = _db.get_place_from_handle(place_handle) + return self.display(_db, place, event.get_date_object(), fmt) + return "" - def display(self, db, place, date=None, fmt=-1): + def display(self, _db, place, date=None, fmt=-1): + """ + This is the place title display routine. It displays a place title + according to the format and rules defined in PlaceFormat. + """ if not place: return "" if not config.get('preferences.place-auto'): return place.title - else: - if fmt == -1: - fmt = config.get('preferences.place-format') - pf = self.place_formats[fmt] - lang = pf.language - all_places = get_location_list(db, place, date, lang) - - # Apply format string to place list - index = _find_populated_place(all_places) - places = [] - for slice in pf.levels.split(','): - parts = slice.split(':') - if len(parts) == 1: - offset = _get_offset(parts[0], index) - if offset is not None: - try: - places.append(all_places[offset]) - except IndexError: - pass - elif len(parts) == 2: - start = _get_offset(parts[0], index) - end = _get_offset(parts[1], index) - if start is None: - places.extend(all_places[:end]) - elif end is None: - places.extend(all_places[start:]) + if fmt == -1: + fmt = config.get('preferences.place-format') + if fmt > len(self.place_formats) - 1: + config.set('preferences.place-format', 0) + if fmt > len(self.place_formats) - 1: + fmt = 0 + _pf = self.place_formats[fmt] + # sort the rules by hierarchy so we can do one hierarchy at a time + hiers = [PlaceHierType()] + rulesets = [[]] + for rule in _pf.rules: + for hier, rules in zip(hiers, rulesets): + if hier == rule.hier: + rules.append(rule) + break + else: + hiers.append(rule.hier) + rulesets.append([rule]) + lang = _pf.language + + # process each hierarchy, starting with ADMIN + title = names = '' + for hier, rules in zip(hiers, rulesets): + + all_places = get_location_list(_db, place, date, lang, hier=hier) + + # Apply format to place list + # if ADMIN, start with everything shown, otherwise nothing shown + # val[0] is the place name + if hier == PlaceHierType.ADMIN: + places = [val[0] for val in all_places] + else: + places = [None] * len(all_places) + for rule in rules: + if rule.where: + # this rule applies to a specific place + for plac in all_places: + if plac[2] == rule.where: # test if handles match + break # rule is good for this place + else: # no match found for handle + continue # skip this rule + first = False + if rule.type == PlaceRule.T_GRP: + # plac[4] and rule.value are PlaceGroupType + if rule.vis == PlaceRule.V_LARGE: + # go from largest down + for indx in range(len(all_places) - 1, -1, -1): + plac = all_places[indx] + # plac[4] is a PlaceGroupType + if plac[4] == rule.value: + # match on group + if first: # If we found first one already + places[indx] = None # remove this one + else: + first = True + self._show(plac, rule, places, indx) else: - places.extend(all_places[start:end]) - - if pf.street: - types = [item[1] for item in places] - try: - idx = types.index(PlaceType.NUMBER) - except ValueError: - idx = None - if idx is not None and len(places) > idx+1: - if pf.street == 1: - combined = (places[idx][0] + ' ' + places[idx+1][0], - places[idx+1][1]) + # work from smallest up + for indx, plac in enumerate(all_places): + if plac[4] == rule.value: + # match on group + if rule.vis == PlaceRule.V_SMALL: + if first: # If we found first one already + places[indx] = None # remove this one + else: + first = True + self._show(plac, rule, places, indx) + elif rule.vis == PlaceRule.V_ALL: + self._show(plac, rule, places, indx) + else: # rule.vis == PlaceRule.V_NONE: + places[indx] = None # remove this one + elif rule.type == PlaceRule.T_TYP: + # plac[1] and rule.value are PlaceType + if rule.vis == PlaceRule.V_LARGE: + # go from largest down + for indx in range(len(all_places) - 1, -1, -1): + plac = all_places[indx] + if plac[1].is_same(rule.value): + # match on ptype + if first: # If we found first one already + places[indx] = None # remove this one + else: + first = True + self._show(plac, rule, places, indx) else: - combined = (places[idx+1][0] + ' ' + places[idx][0], - places[idx+1][1]) - places = places[:idx] + [combined] + places[idx+2:] + # work from smallest up + for indx, plac in enumerate(all_places): + if plac[1].is_same(rule.value): + # match on group + if rule.vis == PlaceRule.V_SMALL: + if first: # If we found first one already + places[indx] = None # remove this one + else: + first = True + self._show(plac, rule, places, indx) + elif rule.vis == PlaceRule.V_ALL: + self._show(plac, rule, places, indx) + else: # rule.vis == PlaceRule.V_NONE: + places[indx] = None # remove this one + else: + # we have a rule about street/number + _st = _num = None + for indx, plac in enumerate(all_places): + # plac[1] is PlaceType + p_type = plac[1].pt_id + if((p_type == "Street" or p_type == "Number") and + rule.vis == PlaceRule.V_NONE): + places[indx] = None # remove this one + elif p_type == "Street": + _st = indx + places[indx] = plac[0] + elif p_type == "Number": + _num = indx + places[indx] = plac[0] + if _st is not None and _num is not None: + if((rule.vis == PlaceRule.V_NUMST and _num < _st) or + (rule.vis == PlaceRule.V_STNUM and _num > _st)): + continue # done with rule + # need to swap final names + street = places[_st] + places[_st] = places[_num] + places[_num] = street + + # For the ADMIN hierarchy, make sure that the smallest place is + # included for places not deeply enclosed. + if hier == PlaceHierType.ADMIN: + s_groups = [P_G.PLACE, P_G.UNPOP, P_G.NONE, P_G.BUILDING] + if places[0] is None and all_places[0][4] not in s_groups: + places[0] = all_places[0][0] + + names = '' + for indx in (range(len(all_places) - 1, -1, -1) if _pf.reverse else + range(len(all_places))): + name = places[indx] + if name is not None: + names += (_(", ") + name) if names else name + if title and names: + title += _('; ') + title += names - names = [item[0] for item in places] - if pf.reverse: - names.reverse() + return title - # TODO for Arabic, should the next line's comma be translated? - return ", ".join(names) + @staticmethod + def _show(place, rule, places, indx): + """ + Place is to be shown, but need to deal with abbreviations. + place is a tuple of place to show + rule.abb is the selected abbreviation instruction + places is list of place tuples to show + """ + do_abb = int(rule.abb) + name, dummy_type, dummy_hndl, abblist, _group = place + if do_abb == PlaceRule.A_FIRST: + if abblist: + name = abblist[0].get_value() + elif do_abb != PlaceRule.A_NONE: + for abb in abblist: + if rule.abb == abb.get_type(): + name = abb.get_value() + places[indx] = name def get_formats(self): + """ return the available formats as a list """ return self.place_formats def set_formats(self, formats): + """ set a list of place formats """ self.place_formats = formats - def load_formats(self): - dom = xml.dom.minidom.parse(PLACE_FORMATS) - top = dom.getElementsByTagName('place_formats') + def load_formats(self, formats): + """ + :param formats: list of formats from db metadata. + :type formats: list of PlaceFormat objects. - for fmt in top[0].getElementsByTagName('format'): - name = fmt.attributes['name'].value - levels = fmt.attributes['levels'].value - language = fmt.attributes['language'].value - street = int(fmt.attributes['street'].value) - reverse = fmt.attributes['reverse'].value == 'True' - pf = PlaceFormat(name, levels, language, street, reverse) - self.place_formats.append(pf) + load formats obtained from db, and from the saved json file. - dom.unlink() + If the current session has no user formats, we will load the last + saved json formats to form an initial format set. + + If the current session already has user formats, from a prior opened + db, we will keep them to form an initial format set. + + If the newly opened db making this call has a named format we will + not change it, and update the session to use it. + + If the current contents of place formats from this session has new user + formats, they will end up in the db. + """ + if len(self.place_formats) == 1 and os.path.exists(PLACE_FORMATS): + try: + with open(PLACE_FORMATS, 'r', encoding='utf8') as _fp: + load_formats = json.load(_fp, object_hook=_object_hook) + except BaseException: + print(_("Error in '%s' file: cannot load.") % PLACE_FORMATS) + for indx, fmt in enumerate(load_formats): + # if not right version, just drop the format + if(not hasattr(fmt, 'version') or fmt.version != PFVERS or + not indx): + continue + self.place_formats.append(fmt) + for indx, fmt in enumerate(formats): + # if not right version, just drop the format + if(not hasattr(fmt, 'version') or fmt.version != PFVERS or + not indx): + continue + for index in range(len(self.place_formats)): + if fmt.name == self.place_formats[index].name: + self.place_formats[index] = fmt + break + else: + self.place_formats.append(fmt) def save_formats(self): - doc = xml.dom.minidom.Document() - place_formats = doc.createElement('place_formats') - doc.appendChild(place_formats) - for fmt in self.place_formats: - node = doc.createElement('format') - place_formats.appendChild(node) - node.setAttribute('name', fmt.name) - node.setAttribute('levels', fmt.levels) - node.setAttribute('language', fmt.language) - node.setAttribute('street', str(fmt.street)) - node.setAttribute('reverse', str(fmt.reverse)) - with open(PLACE_FORMATS, 'w', encoding='utf-8') as f_d: - doc.writexml(f_d, addindent=' ', newl='\n', encoding='utf-8') - - -def _get_offset(value, index): - if index is not None and value.startswith('p'): - try: - offset = int(value[1:]) - except ValueError: - offset = 0 - offset += index - else: - try: - offset = int(value) - except ValueError: - offset = None - return offset - -def _find_populated_place(places): - populated_place = None - for index, item in enumerate(places): - if int(item[1]) in [PlaceType.HAMLET, PlaceType.VILLAGE, - PlaceType.TOWN, PlaceType.CITY]: - populated_place = index - return populated_place + """ this saves the current format set to the json file. Saving to the + db is done elsewhere. + """ + if len(self.place_formats) > 1: + try: + with open(PLACE_FORMATS, 'w', encoding='utf8') as _fp: + json.dump(self.place_formats, _fp, indent=2, + default=_default, ensure_ascii=False, + sort_keys=True) + except BaseException: + print(_("Failure writing %s") % PLACE_FORMATS) + displayer = PlaceDisplay() diff --git a/gramps/gen/utils/location.py b/gramps/gen/utils/location.py index a9972f4c33c..68f23866d77 100644 --- a/gramps/gen/utils/location.py +++ b/gramps/gen/utils/location.py @@ -22,25 +22,55 @@ Location utility functions """ from ..lib.date import Date, Today +from ..lib.placetype import PlaceType +from ..lib.placegrouptype import PlaceGroupType as P_G +from ..lib.placehiertype import PlaceHierType +from ..lib.attrtype import AttributeType +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext + #------------------------------------------------------------------------- # # get_location_list # #------------------------------------------------------------------------- -def get_location_list(db, place, date=None, lang=''): +def get_location_list(db, place, date=None, lang='', hier=PlaceHierType.ADMIN): """ - Return a list of place names for display. + Return a list of place tuples for display: + name str (0) + PlaceType (1) + Place handle (2) + Place abbr (3) + Place Group (4) + + the list is in order of smallest (most enclosed) to largest place. + The list will match the date, lang and hierarchy type. + :param place: the place + :type place: Place + :param date: the date to display or None for latest + :type date: Date + :param lang: the language value to use for the string ('en', 'en_GB') + :type lang: str + :returns: a list of place tuples for display: + name str (0) + PlaceType (1) + Place handle (2) + Place abbr (3) + Place group (4) + :rtype: list """ if date is None: date = __get_latest_date(place) visited = [place.handle] - lines = [(__get_name(place, date, lang), place.get_type())] + name, abbrs = __get_name(place, date, lang) + lines = [(name, __get_type(place, date), place.handle, abbrs, place.group)] while True: handle = None for placeref in place.get_placeref_list(): ref_date = placeref.get_date_object() - if ref_date.is_empty() or date.match_exact(ref_date): + if placeref.get_type() == hier and (ref_date.is_empty() or + date.match_exact(ref_date)): handle = placeref.ref break if handle is None or handle in visited: @@ -49,23 +79,57 @@ def get_location_list(db, place, date=None, lang=''): if place is None: break visited.append(handle) - lines.append((__get_name(place, date, lang), place.get_type())) + name, abbrs = __get_name(place, date, lang) + lines.append((name, __get_type(place, date), place.handle, + abbrs, place.group)) return lines + +def get_placetype_str(place, date=None, locale=None): + """ + gets the PlaceType display string from a place + :param place: the place + :type place: Place + :param date: the date to display or None for latest + :type date: Date + :param lang: the language value to use for the string ('en', 'en_GB') + :type lang: str + :returns: display string of the place type + :rtype: str + """ + if date is None: + date = __get_latest_date(place) + ptype = __get_type(place, date) + return ptype.str(locale=locale) + + def __get_name(place, date, lang): + """ Gets the name for a given date and language. Returns the str name and + a list of abbreviations (PlaceAbbrevType) + """ endonym = None - for place_name in place.get_all_names(): + for place_name in place.get_names(): name_date = place_name.get_date_object() if name_date.is_empty() or date.match_exact(name_date): if place_name.get_language() == lang: - return place_name.get_value() + return place_name.get_value(), place_name.get_abbrevs() if endonym is None: endonym = place_name.get_value() - return endonym if endonym is not None else '?' + abbs = place_name.get_abbrevs() + return (endonym, abbs) if endonym is not None else ('?', []) + + +def __get_type(place, date): + for place_type in place.get_types(): + type_date = place_type.get_date_object() + if type_date.is_empty() or date.match_exact(type_date): + return place_type + return PlaceType(PlaceType.UNKNOWN) + def __get_latest_date(place): latest_date = None - for place_name in place.get_all_names(): + for place_name in place.get_names(): date = place_name.get_date_object() if date.is_empty() or date.modifier == Date.MOD_AFTER: return Today() @@ -79,6 +143,7 @@ def __get_latest_date(place): latest_date = date return latest_date + #------------------------------------------------------------------------- # # get_main_location @@ -88,41 +153,33 @@ def get_main_location(db, place, date=None): """ Find all places in the hierarchy above the given place, and return the result as a dictionary of place types and names. - """ - return dict([(int(place_type), name) - for name, place_type - in get_location_list(db, place, date) - if not place_type.is_custom()]) -#------------------------------------------------------------------------- -# -# get_locations -# -#------------------------------------------------------------------------- -def get_locations(db, place): - """ - Determine each possible route up the place hierarchy, and return a list - containing dictionaries of place types and names. + This is used by several legacy addons and reports. For better + compatibility we will utilize some of the place groups to synthesize this. + + Returns only Country, State, County, Parish, City, and Locality """ - locations = [] - todo = [(place, [(int(place.get_type()), __get_all_names(place))], - [place.handle])] - while len(todo): - place, tree, visited = todo.pop() - for parent in place.get_placeref_list(): - if parent.ref not in visited: - parent_place = db.get_place_from_handle(parent.ref) - if parent_place is not None: - parent_tree = tree + [(int(parent_place.get_type()), - __get_all_names(parent_place))] - parent_visited = visited + [parent.ref] - todo.append((parent_place, parent_tree, parent_visited)) - if len(place.get_placeref_list()) == 0: - locations.append(dict(tree)) - return locations + ret = {} + for (name, p_type, _hndl, _abbrs, group) in get_location_list(db, place, + date): + if group == P_G.COUNTRY: + ret["Country"] = name + elif p_type == "County" : + ret["County"] = name + elif p_type == PlaceType.CUSTOM : + continue + elif p_type == "Parish" : + ret["Parish"] = name + elif p_type == "Locality" : + ret["Locality"] = name + elif p_type == "Street" : + ret["Street"] = name + elif group == P_G.REGION: + ret["State"] = name + elif group == P_G.PLACE: + ret["City"] = name + return ret -def __get_all_names(place): - return [name.get_value() for name in place.get_all_names()] #------------------------------------------------------------------------- # @@ -147,3 +204,19 @@ def located_in(db, handle1, handle2): parent_visited = visited + [parent.ref] todo.append((parent_place, parent_visited)) return False + + +#------------------------------------------------------------------------- +# +# get_code (postal code) +# +#------------------------------------------------------------------------- +def get_code(place): + """ + Returns the postal code(s) from a place that are found in attributes. + """ + txt = '' + for attr in place.get_attribute_list(): + if attr.type == AttributeType.POSTAL and attr.value: + txt += (_(", ") if txt else '') + attr.value + return txt diff --git a/gramps/gui/configure.py b/gramps/gui/configure.py index b6a545015d3..b48271a1eb7 100644 --- a/gramps/gui/configure.py +++ b/gramps/gui/configure.py @@ -67,6 +67,7 @@ from .dialog import ErrorDialog, OkDialog from .editors.editplaceformat import EditPlaceFormat from .display import display_help +from .glade import Glade from gramps.gen.plug.utils import available_updates from .plug import PluginWindows #from gramps.gen.errors import WindowActiveError @@ -571,12 +572,16 @@ def __init__(self, uistate, dbstate): ) ConfigureDialog.__init__(self, uistate, dbstate, page_funcs, GrampsPreferences, config, - on_close=update_constants) + on_close=self._close) + self.panel.set_current_page(0) help_btn = self.window.add_button(_('_Help'), Gtk.ResponseType.HELP) help_btn.connect( 'clicked', lambda x: display_help(WIKI_HELP_PAGE, WIKI_HELP_SEC)) self.setup_configs('interface.grampspreferences', 700, 450) + def _close(self): + update_constants() # save the age constants + def create_grid(self): """ Gtk.Grid for config panels (tabs). @@ -1424,11 +1429,15 @@ def cb_place_fmt_rebuild(self): """ Called to rebuild the place format list. """ + active = self.pformat.get_active() model = Gtk.ListStore(str) for fmt in _pd.get_formats(): model.append([fmt.name]) self.pformat.set_model(model) - self.pformat.set_active(0) + if active != -1 and active < len(model): + self.pformat.set_active(active) + else: + self.pformat.set_active(0) def check_for_type_changed(self, obj): active = obj.get_active() diff --git a/gramps/gui/editors/editplaceformat.py b/gramps/gui/editors/editplaceformat.py index b1e4579632d..f4c4bc26e44 100644 --- a/gramps/gui/editors/editplaceformat.py +++ b/gramps/gui/editors/editplaceformat.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2017 Nick Hall +# Copyright (C) 2019 Paul Culley # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +25,8 @@ # #------------------------------------------------------------------------- from gi.repository import Gtk +import re +import json #------------------------------------------------------------------------- # @@ -32,12 +35,26 @@ #------------------------------------------------------------------------- from ..managedwindow import ManagedWindow from ..glade import Glade -from ..listmodel import ListModel -from gramps.gen.errors import ValidationError +from ..listmodel import ListModel, NOSORT, INTEGER +from ..autocomp import StandardCustomSelector +from ..widgets import PlaceTypeSelector +from ..selectors.selectplace import SelectPlace +from gramps.gen.config import config +from gramps.gen.lib import (PlaceType, PlaceHierType, PlaceAbbrevType, + PlaceGroupType) +from gramps.gen.errors import HandleError from gramps.gen.display.place import displayer as _pd -from gramps.gen.display.place import PlaceFormat +from gramps.gen.display.place import PlaceFormat, PlaceRule from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext +# gets the prefix, number, suffix specified in a format string, eg: +# P%04dX returns 'P', '04', 'X' It has to have the integer format with at +# least 3 digits to pass. +_parseformat = re.compile(r'(^[^\d]*)%(0[3-9])d([^\d]*$)') +# finds prefix, number, suffix of a Gramps ID ignoring a leading or +# trailing space. The number must be at least three digits. +_prob_id = re.compile(r'^ *([^\d]*)(\d{3,9})([^\d]*) *$') + #------------------------------------------------------------------------- # @@ -47,35 +64,47 @@ class EditPlaceFormat(ManagedWindow): def __init__(self, uistate, dbstate, track, callback): self.title = _('Place Format Editor') + self.dbstate = dbstate + if not dbstate.is_open(): + return ManagedWindow.__init__(self, uistate, track, EditPlaceFormat) self.callback = callback self.top = Glade() self.set_window(self.top.toplevel, None, self.title, None) - self.setup_configs('interface.editplaceformat', 600, 400) + self.setup_configs('interface.editplaceformat', 750, 400) self.top.get_object('add').connect('clicked', self.__add) self.top.get_object('remove').connect('clicked', self.__remove) self.top.get_object('name').connect('changed', self.__name_changed) - self.top.get_object('levels').connect('validate', self._validate) + self.top.get_object('add_btn').connect('clicked', self.__add_rule) + self.top.get_object('rem_btn').connect('clicked', self.__remove_rule) + self.top.get_object('up_btn').connect('clicked', self.__up_rule) + self.top.get_object('down_btn').connect('clicked', self.__down_rule) + self.top.get_object('infobar').connect('response', self.__info_close) self.window.connect('response', self.__close) self.model = None + self.rule_model = None self.formats = _pd.get_formats() + self.rules = None self.current_format = None + self.current_rule = None + self.hier = None self.__populate_format_list() self.show() - def build_menu_names(self, obj): - return (self.title, None) + def build_menu_names(self, _obj): + return (self.title, self.title) def __populate_format_list(self): flist = self.top.get_object('format_list') self.model = ListModel(flist, - [(_('Format'), -1, 100)], - select_func=self.__format_changed) + [(_('Format'), -1, 100)], + select_func=self.__format_changed) for fmt in self.formats: self.model.add([fmt.name]) self.model.select_row(0) - def __format_changed(self, selection): + def __format_changed(self, _selection): + """ The format changed, update gui for the new format """ if self.current_format is not None: fmt = self.formats[self.current_format] self.__save_format(fmt) @@ -84,74 +113,360 @@ def __format_changed(self, selection): fmt = self.formats[row] self.__load_format(fmt) self.current_format = row - if row == 0: - self.top.get_object('remove').set_sensitive(False) - self.top.get_object('name').set_sensitive(False) - self.top.get_object('levels').set_sensitive(False) - self.top.get_object('street').set_sensitive(False) - self.top.get_object('language').set_sensitive(False) - self.top.get_object('reverse').set_sensitive(False) + self.rules = fmt.rules + for obj in ('remove', 'name', 'language', 'reverse', + 'add_btn', 'rem_btn', 'up_btn', 'down_btn'): + self.top.get_object(obj).set_sensitive(row != 0) + self.__update_rules() + + def __update_rules(self): + if self.rule_model: + self.rule_model.clear() else: - self.top.get_object('remove').set_sensitive(True) - self.top.get_object('name').set_sensitive(True) - self.top.get_object('levels').set_sensitive(True) - self.top.get_object('street').set_sensitive(True) - self.top.get_object('language').set_sensitive(True) - self.top.get_object('reverse').set_sensitive(True) - self.top.get_object('levels').validate(force=True) + self.rule_model = ListModel( + self.top.get_object('rule_view'), + [(_('Where'), NOSORT, 150), + (_('Type'), NOSORT, 150), + (_('Display'), NOSORT, 150), + ('', NOSORT, 150, INTEGER)], + event_func=self.__edit_rule) + for indx, rule in enumerate(self.rules): + where = rule.where + if where: + try: + p_name = self.dbstate.db.get_place_from_handle( + where).get_name().get_value() + except HandleError: + # deal with missing place due to changed db or removed item + self.top.get_object('infobar').show() + p_name = _("Unknown") + else: + p_name = _('All') + r_type = rule.type + if r_type == PlaceRule.T_GRP: + rt_name = _("%s Group") % str(rule.value) + elif r_type == PlaceRule.T_TYP: + rt_name = str(rule.value) + else: + rt_name = _("Street Format") + r_abb = int(rule.abb) + abbr = '' + if rule.vis >= PlaceRule.V_ALL and r_abb != PlaceRule.A_NONE: + if r_abb == PlaceRule.A_FIRST: + abbr = _("First") + else: + abbr = str(rule.abb) + if abbr and rule.hier == PlaceHierType.ADMIN: + vis = _("{r_vis}, Abbrev: {abbr}").format( + r_vis=PlaceRule.VIS_MAP[rule.vis], abbr=abbr) + elif abbr: + vis = _("{hier}; {r_vis}, Abbrev: {abbr}").format( + r_vis=PlaceRule.VIS_MAP[rule.vis], abbr=abbr, + hier=str(rule.hier)) + elif rule.hier != PlaceHierType.ADMIN: + vis = _("{hier}; {r_vis}").format( + r_vis=PlaceRule.VIS_MAP[rule.vis], hier=str(rule.hier)) + else: + vis = PlaceRule.VIS_MAP[rule.vis] + self.rule_model.add((p_name, rt_name, vis, indx)) def __name_changed(self, entry): - store, iter_ = self.model.get_selected() + dummy_store, iter_ = self.model.get_selected() self.model.set(iter_, [entry.get_text()]) - def _validate(self, widget, text): - for level in text.split(','): - parts = level.split(':') - if len(parts) < 1: - return ValidationError('Empty level') - if len(parts) > 2: - return ValidationError('Invalid slice') - for part in parts: - integer_str = part.replace('p', '') - if integer_str != '': - try: - integer = int(integer_str) - except ValueError: - return ValidationError('Invalid format string') - def __load_format(self, fmt): self.top.get_object('name').set_text(fmt.name) - self.top.get_object('levels').set_text(fmt.levels) - self.top.get_object('street').set_active(fmt.street) self.top.get_object('language').set_text(fmt.language) self.top.get_object('reverse').set_active(fmt.reverse) def __save_format(self, fmt): fmt.name = self.top.get_object('name').get_text() - fmt.levels = self.top.get_object('levels').get_text() - fmt.street = self.top.get_object('street').get_active() fmt.language = self.top.get_object('language').get_text() fmt.reverse = self.top.get_object('reverse').get_active() - def __add(self, button): + def __add(self, _button): name = _('New') - self.formats.append(PlaceFormat(name, ':', '', 0, False)) + self.formats.append( + PlaceFormat(name, '', False, [])) self.model.add([name]) - self.model.select_row(len(self.formats)-1) + self.model.select_row(len(self.formats) - 1) - def __remove(self, button): - store, iter_ = self.model.get_selected() + def __remove(self, _button): + dummy_store, iter_ = self.model.get_selected() if iter_: self.current_format = None del self.formats[self.model.get_selected_row()] self.model.remove(iter_) if self.model.get_selected_row() == -1: - self.model.select_row(len(self.formats)-1) + self.model.select_row(len(self.formats) - 1) - def __close(self, *obj): - row = self.model.get_selected_row() + def __close(self, *_obj): + """ Save or abandon work """ fmt = self.formats[self.current_format] self.__save_format(fmt) + formats = _pd.get_formats() + self.dbstate.db.save_place_formats(formats) _pd.save_formats() self.callback() self.close() + + def __add_rule(self, _button): + """ Add a new rule """ + rule = PlaceRule(None, PlaceRule.T_GRP, + PlaceGroupType(PlaceGroupType.COUNTRY), + PlaceRule.V_SMALL, + PlaceAbbrevType(PlaceRule.A_NONE), + PlaceHierType(PlaceHierType.ADMIN)) + self.rules.append(rule) + EditPlaceRule(self.uistate, self.dbstate, self.track, rule, + self.edit_rule_callback) + + def __edit_rule(self, _obj): + """ edit the selected rule """ + row = self.rule_model.get_selected_row() + EditPlaceRule(self.uistate, self.dbstate, self.track, self.rules[row], + self.edit_rule_callback) + + def edit_rule_callback(self): + """ update the ui with rule data """ + self.__update_rules() + + def __remove_rule(self, _button): + """ Remove a rule """ + dummy_store, iter_ = self.rule_model.get_selected() + if iter_: + del self.rules[self.rule_model.get_selected_row()] + self.rule_model.remove(iter_) + if self.rule_model.get_selected_row() == -1: + self.rule_model.select_row(len(self.formats) - 1) + + def __up_rule(self, _button): + """ Move a rule up in the list """ + row = self.rule_model.get_selected_row() + if row < 1: + return + self.rule_model.move_up(row) + self.rules.insert(row, self.rules.pop(row)) + + def __down_rule(self, _button): + """ Move a rule down in list """ + row = self.rule_model.get_selected_row() + if row >= len(self.rules) - 1 or row == -1: + return + self.rule_model.move_down(row) + self.rules.insert(row + 1, self.rules.pop(row)) + + def __info_close(self, widget, _resp): + """ hide the infobar when closed """ + widget.hide() + + +#------------------------------------------------------------------------- +# +# EditPlaceRule +# +#------------------------------------------------------------------------- +class EditPlaceRule(ManagedWindow): + """ Editor for Place Rules """ + def __init__(self, uistate, dbstate, track, rule, callback): + self.db = dbstate.db + self.dbstate = dbstate + self.rule = rule + self.where = None # temp storage for "where" place handle + self.where_id = '' # temp storage for "where" place gramps_id + self.where_title = '' # temp storage for "where" place title + self.ptype = None + self.title = _('Place Format Rule Editor') + ManagedWindow.__init__(self, uistate, track, EditPlaceRule, modal=True) + self.callback = callback + self.top = Glade(toplevel='rule_dlg', also_load=['image1']) + self.set_window(self.top.toplevel, None, self.title, None) + self.setup_configs('interface.editplacerule', 500, 400) + self.top.get_object('all_btn').connect('clicked', self.__all_btn) + self.top.get_object('place_btn').connect('clicked', self.__place_btn) + self.window.connect('response', self.__done) + # Set up hierarchy + hier = self.top.get_object('hier_cbe') + custom_hier_types = sorted(self.dbstate.db.get_placehier_types(), + key=lambda s: s.lower()) + mapping = PlaceHierType.get_map(PlaceHierType) + self.hier = StandardCustomSelector( + mapping, hier, custom_key=PlaceHierType.CUSTOM, + additional=custom_hier_types, ) + if rule.hier: + self.hier.set_values((int(rule.hier), str(rule.hier))) + self.type_combo = None + # set 'where' label + self.where_cb = self.top.get_object('where_cb') + if rule.where: + try: + p_name = self.db.get_place_from_handle( + rule.where).get_name().get_value() + except HandleError: + p_name = self._find_place_in_db() + else: + p_name = _('All Places') + self.top.get_object('where_lbl').set_text(p_name) + # set up what combo + what_cb = self.top.get_object('what_cb') + what_cb.connect('changed', self.__what_changed) + what_cb.set_active_id(str(rule.type)) + # set up type and vis combo + self.vis_cb = self.top.get_object('vis_cb') + self.__what_changed(what_cb, use_val=True) + # set up abb combo + abb = self.top.get_object('abbrev_cb') + custom = sorted(self.db.get_placeabbr_types(), + key=lambda s: s.lower()) + mapping = PlaceAbbrevType.get_map(PlaceAbbrevType).copy() + mapping[-2] = _("None") + mapping[-3] = _("First") + self.abb_combo = StandardCustomSelector( + mapping, cbe=abb, custom_key=PlaceAbbrevType.CUSTOM, + additional=custom) + self.abb_combo.set_values((int(rule.abb), str(rule.abb))) + self.show() + + def build_menu_names(self, _obj): + return (self.title, self.title) + + def __all_btn(self, _obj): + """ 'Where/All' button clicked, set rule for all places + """ + self.where = None + self.top.get_object('where_lbl').set_text(_('All Places')) + + def __place_btn(self, _obj): + """ Select the place for this rule """ + sel = SelectPlace(self.dbstate, self.uistate, self.track) + place = sel.run() + if place: + self.where = place.handle + self.where_id = place.gramps_id + self.where_title = _pd.display(self.db, place, fmt=0) + name = place.get_name().get_value() + self.top.get_object('where_lbl').set_text(name) + + def __what_changed(self, _obj, use_val=False): + """ Change type combo and vis combo based on what combo setting + """ + r_type = _obj.get_active_id() + if not r_type: + return + r_type = int(r_type) + def_vis = PlaceRule.V_NONE + type_cb = self.top.get_object('type_cbe') # for GrampsTypeSelectors + type_cb1 = self.top.get_object('type_cbe1') # for PlaceTypeSelector + vis_lst = (PlaceRule.V_NONE, PlaceRule.V_ALL, + PlaceRule.V_SMALL, PlaceRule.V_LARGE) + if r_type == PlaceRule.T_ST_NUM: + self.top.get_object('type_lbl').set_text('') + type_cb.get_child().set_text('') + type_cb.set_sensitive(False) + type_cb1.hide() + type_cb.show() + self.top.get_object('abbrev_cb').get_child().set_text('') + self.top.get_object('abbrev_cb').set_sensitive(False) + vis_lst = (PlaceRule.V_NONE, PlaceRule.V_STNUM, PlaceRule.V_NUMST) + def_vis = PlaceRule.V_NUMST + elif r_type == PlaceRule.T_GRP: + self.top.get_object('type_lbl').set_text(_('Group:')) + type_cb.set_sensitive(True) + type_cb1.hide() + type_cb.show() + self.top.get_object('abbrev_cb').set_sensitive(True) + self.type_combo = StandardCustomSelector( + PlaceGroupType().get_map(), type_cb, + custom_key=PlaceGroupType.CUSTOM, + additional=self.dbstate.db.get_placegroup_types()) + value = self.rule.value if use_val else PlaceGroupType( + PlaceGroupType.PLACE) + self.type_combo.set_values((value.value, value.string)) + elif r_type == PlaceRule.T_TYP: + self.top.get_object('type_lbl').set_text(_('Type:')) + type_cb.set_sensitive(True) + type_cb.hide() + type_cb1.show() + self.top.get_object('abbrev_cb').set_sensitive(True) + + self.ptype = (self.rule.value if use_val + else PlaceType(PlaceType.UNKNOWN)) + self.type_combo = PlaceTypeSelector(self.dbstate, + type_cb1, self.ptype) + self.vis_cb.remove_all() + for vis in vis_lst: + self.vis_cb.append(str(vis), PlaceRule.VIS_MAP[vis]) + if not self.vis_cb.set_active_id(str(self.rule.vis)): + self.vis_cb.set_active_id(str(def_vis)) + + def __done(self, _obj, response): + """ Dialog is closing """ + if response == Gtk.ResponseType.OK: + # need to save rule data + self.rule.hier = PlaceHierType(self.hier.get_values()) + abb = self.abb_combo.get_values() + self.rule.abb = PlaceAbbrevType(abb) + self.rule.where = self.where + self.rule.where_id = self.where_id + self.rule.where_title = self.where_title + self.rule.type = int(self.top.get_object('what_cb'). + get_active_id()) + if self.rule.type == PlaceRule.T_ST_NUM: + self.rule.abb = PlaceAbbrevType(-2) + elif self.rule.type == PlaceRule.T_GRP: + self.rule.value = PlaceGroupType(self.type_combo.get_values()) + else: # rule.type == PlaceRule.T_TYPE + self.rule.value = self.ptype + self.rule.vis = int(self.vis_cb.get_active_id()) + self.callback() + self.close() + + def is_unique_gid(self, gramps_id): + """ try to determine if this gramps ID is unique; that it is probably + a GOV or GeoNames ID or similar. + Works by calling it unique if it is NOT similar to a standard + Gramps ID or the currently set place ID format. + + Returns True if unique + """ + # get current format and determine prefix/suffix + p_id = config.get('preferences.pprefix') + formatmatch = _parseformat.match(p_id) + if formatmatch: + prefix = formatmatch.groups()[0] + # width_fmt = int(formatmatch.groups()[1]) + suffix = formatmatch.groups()[2] + else: # not a legal format string, use default + prefix = 'P' + # width_fmt = 4 + suffix = '' + # test incoming gid to find its prefix/suffix and compare + match = _prob_id.match(gramps_id) + return not (match and + (prefix == match.groups()[0] and + suffix == match.groups()[2] or + len(match.groups()[0]) == 1 and + len(match.groups()[2]) == 0)) + + def _find_place_in_db(self): + """ Try to find a place in the current db based on its gramps_id + (If it is likely unique) or its title. + + returns Place name if found, else "Previous Title" + """ + if(self.is_unique_gid(self.rule.where_id) and + self.db.has_place_gramps_id(self.rule.where_id)): + place = self.db.get_place_from_gramps_id(self.rule.where_id) + self.where = place.handle + self.where_id = place.gramps_id + self.where_title = _pd.display(self.db, place, fmt=0) + return place.get_name().get_value() + + for place in self.db.iter_places(): + title = _pd.display(self.db, place, fmt=0) + if self.rule.where_title == title: + self.where = place.handle + self.where_id = place.gramps_id + self.where_title = _pd.display(self.db, place, fmt=0) + return place.get_name().get_value() + return _("") + self.rule.where_title diff --git a/gramps/gui/glade/editplaceformat.glade b/gramps/gui/glade/editplaceformat.glade index 3d80c2d54ce..6dce875dc62 100644 --- a/gramps/gui/glade/editplaceformat.glade +++ b/gramps/gui/glade/editplaceformat.glade @@ -1,11 +1,13 @@ - + - - + False dialog + + + False @@ -15,9 +17,6 @@ False end - - - _Close @@ -93,13 +92,10 @@ 1 - - - False - True + False 1 @@ -121,23 +117,79 @@ True False start - Levels: + 7 + Language: + + + 0 + 2 + + + + + True + True + True + + + 1 + 2 + + + + + Reverse display order + True + True + False + start + start + True + True + + + 1 + 3 + + + + + True + False + start + 6 + Name: 0 1 + + + True + True + True + + + 1 + 1 + + True False start - Street format: + General + + + 0 - 2 + 0 + 2 @@ -145,59 +197,388 @@ True False start - Language: + Formatting + + + 0 - 3 + 4 + 2 - + + True + False + start + 2 + start + + + gtk-add + True + True + True + True + + + True + True + 0 + + + + + gtk-remove + True + True + True + True + + + True + True + 1 + + + + + gtk-go-up + True + True + True + True + + + True + True + 2 + + + + + gtk-go-down + True + True + True + True + + + True + True + 3 + + + + + 0 + 5 + 2 + + + + True True True + True + + + - 1 + 0 + 6 + 2 + + + + + True + False + + + 0 3 - - Reverse display order + + False + True + error + True + + + False + 1 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + A Rule has a "Where" place that is not found! Please edit the rule to select a new place. + True + + + False + True + 0 + + + + + False + False + 0 + + + + + 0 + 7 + 2 + + + + + True + True + + + + + True + True + 1 + + + + + + button1 + + + + True + False + gtk-index + + + False + dialog + + + + + + False + vertical + 2 + + + False + end + + + gtk-ok + True + True + True + True + + + True + True + 0 + + + + + gtk-cancel + True + True + True + True + + + True + True + 1 + + + + + False + False + 2 + + + + + True + False + vertical + 3 + + + True + False + start + 6 + What to display + + + + + + False + True + 0 + + + + + True + False + 5 + 6 + + True - True - False + False + start + 6 + Where: + + + 0 + 1 + + + + + True + False + start + 6 + Type: + + + 0 + 3 + + + + + True + False + start True - True + All 1 - 4 + 1 + + + + + All + True + True + True + + + 2 + 1 + + + + + True + True + True + image1 + True + + + 3 + 1 - + True False + start + 6 + What: + + + 0 + 2 + + + + + True + False + 0 + 0 - None - Number Street - Street Number + Group + Type + Street Number Format 1 2 + 3 - + + True + False + True + True + + + False + + + + + 1 + 3 + 3 + + + + True False start - Name: + 6 + Hierarchy: 0 @@ -205,50 +586,146 @@ - + True - True + False True + True + + + True + + 1 0 + 3 - + True - True + False True + True + + + True + + 1 + 3 + 3 + + + + + False + True + 1 + + + + + True + False + start + 6 + Display + + + + + + False + True + 2 + + + + + True + False + 5 + 3 + + + True + False + start + 6 + Visibility: + + + 0 + 0 + + + + + True + False + start + 6 + Abbreviation: + + + 0 1 - + + True + False + True + + + 1 + 0 + 2 + + + + + True + False + True + True + + + False + + + + + 1 + 1 + 2 + - True - True + False + True + 3 - True + False True - 1 + 0 - button1 + ok_btn + cancel_btn - - - From d67eb2b95c4b2dcee4cc8f433a975f38372cd6ce Mon Sep 17 00:00:00 2001 From: prculley Date: Thu, 31 Aug 2017 17:11:59 -0500 Subject: [PATCH 03/26] Refactor Gedcom import to remove alt-location and order dependencies Also improves PLAC.FORM handling, and adds ADR3 tag support --- data/tests/imp_CustTags.gramps | 160 ++--- data/tests/imp_Paris.gramps | 677 ++++++++++---------- data/tests/imp_PhonFax_dfs.gramps | 96 +-- data/tests/imp_bug_8322_test.gramps | 420 ++++++------- data/tests/imp_notetest_dfs.gramps | 99 ++- data/tests/imp_place_test.ged | 100 +++ data/tests/imp_place_test.gramps | 225 +++++++ data/tests/imp_sample.ged | 5 +- data/tests/imp_sample.gramps | 865 +++++++++++++------------- gramps/plugins/lib/libgedcom.py | 916 +++++++++++++--------------- gramps/plugins/test/imports_test.py | 1 + 11 files changed, 1913 insertions(+), 1651 deletions(-) create mode 100644 data/tests/imp_place_test.ged create mode 100644 data/tests/imp_place_test.gramps diff --git a/data/tests/imp_CustTags.gramps b/data/tests/imp_CustTags.gramps index 40cc7b00af1..cd1d19424b8 100644 --- a/data/tests/imp_CustTags.gramps +++ b/data/tests/imp_CustTags.gramps @@ -3,7 +3,7 @@ "http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
- + The Subm /Tester/ 123 Main St. @@ -13,56 +13,56 @@
- + Birth - + - + Circumcision - + - + Elected - + Small town - + _XYZ He should keep his zipper closed - + Independence Day Celebration - + Residence - - + + - + Marriage - + - + _INFIDELITY - + - + Separation - + - + U The @@ -70,43 +70,43 @@ - - - + - + + + - + U Mrs Tester - - + + - + U Tom Tester - + - + - - - - - + + + + + - + 777, record for The Tester 2 @@ -114,83 +114,93 @@ - + Ohio Births, 1958-2002 - + - - 123 High St, Cleveland, Cuyahoga, Ohio, USA, 44140 + + USA + + + + Ohio, USA + + + + + Cuyahoga, Ohio, USA + + + + + Cleveland, Cuyahoga, Ohio, USA + + + + + 123 High St, Cleveland, Cuyahoga, Ohio, USA 44140 - + - - 456 Main St, Cleveland, Cuyahoga, Ohio, USA, 44140 + + 456 Main St, Cleveland, Cuyahoga, Ohio, USA 44140 - + - + Littetown, Smallcounty, Ohio, USA - - 123 Main St., Winslow, PA, 12345 - - + + PA + - + + Winslow, PA + + + + + 123 Main St., Winslow, PA + 12345 + + + + St Rafaels Church, Bay Village, Cuyahoga, Ohio, USA - + Of Ill Repute, Shaker Heights, Ohio, USA - + Cuyahoga, OHIO, USA - - USA - - - - Ohio, USA - - - - - Cuyahoga, Ohio, USA - - - - - Cleveland, Cuyahoga, Ohio, USA - - - - + Testers Repository Library
123 High St., OSF village, CA, USA
- +
- + Because it’s good - + Address as event is legal, with PHON,FAX,EMAIL,WWW - + Testers Repository Record diff --git a/data/tests/imp_Paris.gramps b/data/tests/imp_Paris.gramps index e32f43b448e..e7ac29415d0 100644 --- a/data/tests/imp_Paris.gramps +++ b/data/tests/imp_Paris.gramps @@ -3,719 +3,712 @@ "http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
- + - Paul Culley - 11210 Olde Mint House Ln - Tomball - Tx - USA - 77375 - paulr2787@gmail.com
- + Paris - + - + Ordination - + Notre-Dame-de-Bonne-Nouvelle - + Ordination - + Saint-Benoît - + Ordination - + Saint-Christophe-de-Javel - + Ordination - + Saint-Eustache - + Ordination - + Saint-Germain-des-Prés - + Ordination - + Saint-Germain-L'Auxerrois - + Ordination - + Saint-Gervais-et-Protais - + Ordination - + Saint-Jacques-du-Haut-Pas - + Ordination - + Saint-Laurent - + Ordination - + Saint-Leu-Saint-Gilles - + Ordination - + Saint-Médard - + Ordination - + Saint-Merri - + Ordination - + Saint-Nicolas-de-Chardonnet - + Ordination - + Saint-Nicolas-des-Champs - + Ordination - + Saint-Sauveur - + Ordination - + Saint-Séverin - + Ordination - + Saint-Sulpice - + Ordination - + Catédrale Notre-Dame-de-Paris - + Ordination - + Temple Protestant de L'Oratoire du Louvre - + Ordination - + Saint-Paul - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + Birth - + - + M Arrondissements PARIS - + - + F Églises LUTECE - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + M 01 le Louvre PARIS - - + + - + M 02 la Bourse PARIS - - + + - + M 04 Hôtel-de-Ville PARIS - - + + - + M 09 Opéra PARIS - - + + - + M 05 Panthéon PARIS - - + + - + M 06 Luxembourg PARIS - - + + - + M 07 Palais-Bourbon PARIS - - + + - + M 08 Élysée PARIS - - + + - + M 10 Entrepôt PARIS - - + + - + M 11 Popincourt PARIS - - + + - + M 12 Reuilly PARIS - - + + - + M 13 des Gobelins - la Salpêtrière PARIS - - + + - + M 14 L'Observatoire PARIS - - + + - + M 19 Buttes-Chaumont PARIS - - + + - + M 15 Vaugirard PARIS - - + + - + M 16 Passy PARIS - - + + - + M 17 Batignolles-Monceau PARIS - - + + - + M 18 Buttes-Montmartre PARIS - - + + - + M 03 Temple PARIS - - + + - + M 20 Ménilmontant PARIS - - + + - + - - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - Paris,75056,Paris,Île-de-France,FRANCE, + + FRANCE + + + + Île-de-France, FRANCE + + + + + Paris, Île-de-France, FRANCE + + + + + Paris, Paris, Île-de-France, FRANCE 75056 - + - - Notre-Dame-de-Bonne-Nouvelle,75102,Paris,Île-de-France,FRANCE, + + Notre-Dame-de-Bonne-Nouvelle, Paris, Île-de-France, FRANCE 75102 - + - - Saint-Benoît,75102,Paris,Île-de-France,FRANCE, + + Saint-Benoît, Paris, Île-de-France, FRANCE 75102 - + - - Saint-Christophe-de-Javel,75115,Paris,Île-de-France,FRANCE, + + Saint-Christophe-de-Javel, Paris, Île-de-France, FRANCE 75115 - + - - Saint-Eustache,75101,Paris,Île-de-France,FRANCE, + + Saint-Eustache, Paris, Île-de-France, FRANCE 75101 - + - - Saint-Germain-des-Prés,75114,Paris,Île-de-France,FRANCE, + + Saint-Germain-des-Prés, Paris, Île-de-France, FRANCE 75114 - + - - Saint-Germain-L'Auxerrois,75101,Paris,Île-de-France,FRANCE, + + Saint-Germain-L'Auxerrois, Paris, Île-de-France, FRANCE 75101 - + - - Saint-Gervais-et-Protais,75104,Paris,Île-de-France,FRANCE, + + Saint-Gervais-et-Protais, Paris, Île-de-France, FRANCE 75104 - + - - Saint-Jacques-du-Haut-Pas,75105,Paris,Île-de-France,FRANCE, + + Saint-Jacques-du-Haut-Pas, Paris, Île-de-France, FRANCE 75105 - + - - Saint-Laurent,75110,Paris,Île-de-France,FRANCE, + + Saint-Laurent, Paris, Île-de-France, FRANCE 75110 - + - - Saint-Leu-Saint-Gilles,75101,Paris,Île-de-France,FRANCE, + + Saint-Leu-Saint-Gilles, Paris, Île-de-France, FRANCE 75101 - + - - Saint-Médard,75105,Paris,Île-de-France,FRANCE, + + Saint-Médard, Paris, Île-de-France, FRANCE 75105 - + - - Saint-Merri,75104,Paris,Île-de-France,FRANCE, + + Saint-Merri, Paris, Île-de-France, FRANCE 75104 - + - - Saint-Nicolas-de-Chardonnet,75105,Paris,Île-de-France,FRANCE, + + Saint-Nicolas-de-Chardonnet, Paris, Île-de-France, FRANCE 75105 - + - - Saint-Nicolas-des-Champs,75104,Paris,Île-de-France,FRANCE, + + Saint-Nicolas-des-Champs, Paris, Île-de-France, FRANCE 75104 - + - - Saint-Sauveur,75102,Paris,Île-de-France,FRANCE, + + Saint-Sauveur, Paris, Île-de-France, FRANCE 75102 - + - - Saint-Séverin,75105,Paris,Île-de-France,FRANCE, + + Saint-Séverin, Paris, Île-de-France, FRANCE 75105 - + - - Saint-Sulpice,75106,Paris,Île-de-France,FRANCE, + + Saint-Sulpice, Paris, Île-de-France, FRANCE 75106 - + - - Notre-Dame-de-Paris,75104,Paris,Île-de-France,FRANCE, + + Notre-Dame-de-Paris, Paris, Île-de-France, FRANCE 75104 - + - - Temple Protestant de L'Oratoire du Louvre,75101,Paris,Île-de-France,FRANCE, + + Temple Protestant de L'Oratoire du Louvre, Paris, Île-de-France, FRANCE 75101 - + - - Saint-Paul-Saint-Louis,75104,Paris,Île-de-France,FRANCE, + + Saint-Paul-Saint-Louis, Paris, Île-de-France, FRANCE 75104 - + + + + Ïle-de-France, FRANCE + + - - Paris 01,75101,Paris,Ïle-de-France,FRANCE, + + Paris, Ïle-de-France, FRANCE + + + + + Paris 01, Paris, Ïle-de-France, FRANCE 75101 - + - - Paris 02,75102,Paris,Île-de-France,FRANCE, + + Paris 02, Paris, Île-de-France, FRANCE 75102 - + - - Paris 04,75104,Paris,Île-de-France,FRANCE, + + Paris 04, Paris, Île-de-France, FRANCE 75104 - + - - Paris 09,75109,Paris,Île-de-France,FRANCE, + + Paris 09, Paris, Île-de-France, FRANCE 75109 - + - - Paris 05,75105,Paris,Île-de-France,FRANCE, + + Paris 05, Paris, Île-de-France, FRANCE 75105 - + - - Paris 06,75106,Paris,Île-de-France,FRANCE, + + Paris 06, Paris, Île-de-France, FRANCE 75106 - + - - Paris 07,75107,Paris,Île-de-France,FRANCE, + + Paris 07, Paris, Île-de-France, FRANCE 75107 - + - - Paris 08,75108,Paris,Île-de-France,FRANCE, + + Paris 08, Paris, Île-de-France, FRANCE 75108 - + - - Paris 10,75110,Paris,Île-de-France,FRANCE, + + Paris 10, Paris, Île-de-France, FRANCE 75110 - + - - Paris 11,75111,Paris,Île-de-France,FRANCE, + + Paris 11, Paris, Île-de-France, FRANCE 75111 - + - - Paris 12,75112,Paris,Île-de-France,FRANCE, + + Paris 12, Paris, Île-de-France, FRANCE 75112 - + - - Paris 13,75113,Paris,Île-de-France,FRANCE, + + Paris 13, Paris, Île-de-France, FRANCE 75113 - + - - Paris 14,75114,Paris,Île-de-France,FRANCE, + + Paris 14, Paris, Île-de-France, FRANCE 75114 - + - - Paris 19,75119,Paris,Île-de-France,FRANCE, + + Paris 19, Paris, Île-de-France, FRANCE 75119 - + - - Paris 15,75115,Paris,Île-de-France,FRANCE, + + Paris 15, Paris, Île-de-France, FRANCE 75115 - + - - Paris 16,75116,Paris,Île-de-France,FRANCE, + + Paris 16, Paris, Île-de-France, FRANCE 75116 - + - - Paris 17,75117,Paris,Île-de-France,FRANCE, + + Paris 17, Paris, Île-de-France, FRANCE 75117 - + - - Paris 18,75118,Paris,Île-de-France,FRANCE, + + Paris 18, Paris, Île-de-France, FRANCE 75118 - + - - Paris 03,75103,Paris,Île-de-France,FRANCE, + + Paris 03, Paris, Île-de-France, FRANCE 75103 - + - - Paris 20,75120,Paris,Île-de-France,FRANCE, + + Paris 20, Paris, Île-de-France, FRANCE 75120 - - - - FRANCE - - - - Île-de-France, FRANCE - - - - - Paris, Île-de-France, FRANCE - - - - - Ïle-de-France, FRANCE - - - - - Paris, Ïle-de-France, FRANCE - - +
diff --git a/data/tests/imp_PhonFax_dfs.gramps b/data/tests/imp_PhonFax_dfs.gramps index a8b6ab30b51..26bb2557721 100644 --- a/data/tests/imp_PhonFax_dfs.gramps +++ b/data/tests/imp_PhonFax_dfs.gramps @@ -21,24 +21,24 @@ Birth - + - + Residence - + - + - + Residence @@ -55,16 +55,16 @@ Tester - + - + U Mrs Tester - +
123 Main St. Winslow @@ -76,31 +76,31 @@ - - + + - + U Tom Tester - - + + - + 2 - + 2 - + 2 @@ -122,41 +122,51 @@ - + Ohio Births, 1958-2002 - + - - 123 High St, Cleveland, Cuyahoga, Ohio, USA, 44140 - 44140 - - - - - 123 Main St., Winslow, PA, 12345 - - - - + USA - + Ohio, USA - + - + Cuyahoga, Ohio, USA - + - + Cleveland, Cuyahoga, Ohio, USA - + + + + 123 High St, Cleveland, Cuyahoga, Ohio, USA + 44140 + + + + + PA + + + + Winslow, PA + + + + + 123 Main St., Winslow, PA + 12345 + + @@ -186,7 +196,7 @@ - + Testers Repository Library
@@ -196,8 +206,8 @@ - - + + @@ -221,19 +231,19 @@ Only one phone number supported Line 35: - + Address with PHON,FAX,EMAIL,WWW. attached directly to person is not legal Gedcom, but allowed here. - + Address as event is legal, with PHON,FAX,EMAIL,WWW - + The repository record - + Records not imported into REPO (repository) Gramps ID R0002: Only one phone number supported Line 93: 1 PHON 800-765-4321 diff --git a/data/tests/imp_bug_8322_test.gramps b/data/tests/imp_bug_8322_test.gramps index 3a0edbd355f..8293bce2b4a 100644 --- a/data/tests/imp_bug_8322_test.gramps +++ b/data/tests/imp_bug_8322_test.gramps @@ -3,165 +3,165 @@ "http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
- +
- + Residence - + Residence - + - + Residence - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + @@ -172,321 +172,283 @@ - - - + + + + - + + - - - - - - + + + + + + - - + + - + - + + - - + + - + - - - - + - + the place - + + + - + the address - - - the address - - - - another address - - - - - + another address - - - - the place - - - - + + - + the address - - - the address - - - - - second address - - - - - + the place 2 - - - - second address - - - - - a third address ignored again - - + + the address 2 + + - + place test - - - - - + + + - + address place test - - + + - + + address place test + + + + different place test - - + - + + address place test + + + + address place test - + - + a new place - + address place test - - - - - plus an address just for good measure also ignored - - + + - + address with no place - - + - + Woerden, Zuid-Holland, Netherlands - + - + Kromwijkerkade 63 - + - + Hasselt, Overijssel, Netherlands - + Prinsenstraat 69 - - + + - + Enschede, Overijssel, Netherlands + + - + Calslaan 26-52 - + - + Calslaan 26-44 - - + + - - Enschede, Overijssel, Netherlands - - - - + + Calslaan 26-61 + + - + Calslaan 26-61 - - + Amsterdam, Noord-Holland, Netherlands - + - + Papendrechtstraat 37 - + + - + Olympiaplein 46-2 - - - - - Papendrechtstraat 37 - - - + + - - Amsterdam, Noord-Holland, Netherlands - - + + remembered address that should be set into place + + - + the place created not previously used so changed to add the address; __event_addr(len==0, place is None) - + check that this note is retained when the place is deleted. it should be merged into place - + the place created and then deleted and old data reused; __event_addr(len==0, place is not None) - + setup the place - + the place already exists; but now set doesn't match ; __event_addr(len!=0, place is None) - + the place already exists but now set matches; __event_addr(len!=0, place is not None) - + the address created, then destroyed as we find a matching set; __event_addr(no place_handle, create place) - + address reused, then destroyed as we find a matching set; __event_addr(no place_handle, place found) - - second address ignored - - - second address ignored again - - + ADDR created; __event_place finds it but place does not match; __event_place(len==0, place is None) - + this note is stored with the old address and then merged into the matching place - + ADDR created; __event_place finds it and now place does match; __event_place(len==0, place is not None) - + setup address place test - + address place test found; place exists and can be reused; __event_place(len!=0, place is not None) - + address place test found; but matching addr/plac not found; __event_place(len!=0, place is not None) - + PLAC occurs first; matching entry found; __event_place(no place handle, place is not None) - + PLAC occurs first; matching entry not found; __event_place(no place handle, place is None) - + note is stashed with a Place, and then merged into the address - + Place note - + ADDR note - + PLAC previously encountered, new ADDR, so new Place - + ADDR before PLAC (check ADDR is removed) - + ADDR before PLAC (address matches previous one, then needs to be reassigned) - + PLAC and no ADDR - + PLAC matches previous one, then when ADDR is read need to create a new Place - + PLAC and ADDR match, use existing one - + Records not imported into INDI (individual) Gramps ID I0310: +Line ignored as not understood Line 65: 2 ADDR second address +Skipped subordinate line Line 66: 3 NOTE second address ignored +Line ignored as not understood Line 71: 2 ADDR second address +Skipped subordinate line Line 72: 3 NOTE second address ignored again +Line ignored as not understood Line 73: 2 ADDR a third address ignored again A second PLAC ignored Line 109: 2 PLAC a second PLACe ignored -A second PLAC ignored Line 110: 2 PLAC and a third one also ignored +A second PLAC ignored Line 110: 2 PLAC and a third one also ignored +Line ignored as not understood Line 111: 2 ADDR plus an address just for good measure also ignored +
diff --git a/data/tests/imp_notetest_dfs.gramps b/data/tests/imp_notetest_dfs.gramps index 224012ad171..0c3360130f5 100644 --- a/data/tests/imp_notetest_dfs.gramps +++ b/data/tests/imp_notetest_dfs.gramps @@ -1,13 +1,75 @@ - - + +
- + John A. Tester
+ + + Place + + + + Country + + + Region + + + Region + + + Place + + + Region + + + Place + + + + Region + + + Region + + + Region + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Building + + + @@ -99,10 +161,10 @@ - - - - + + + + @@ -151,9 +213,9 @@ 2 - + 2 - + 2 @@ -199,7 +261,7 @@ - + @SOURCE1@ @@ -212,16 +274,17 @@ - + 123 main, Norwalk, Ohio, USA - + - + Salt Lake City + @@ -328,11 +391,11 @@ Line ignored as not understood Line 46: Birth Event note - + Location Note - + Location Note 2 @@ -438,11 +501,11 @@ Skipped subordinate line Line 133: Name note - + Place note - + LDS xref note diff --git a/data/tests/imp_place_test.ged b/data/tests/imp_place_test.ged new file mode 100644 index 00000000000..cd4a77230ac --- /dev/null +++ b/data/tests/imp_place_test.ged @@ -0,0 +1,100 @@ +0 HEAD +1 SOUR GrampsTests +2 NAME GrampsTests +2 VERS 1.0 +2 CORP GrampsTests, Inc. +3 ADDR PO Box 123 +4 CONT Anyville, UT 12345 +4 CONT USA +3 PHON 1-800-888-8888 +1 DEST GrampsTests +1 DATE 26 AUF 2017 +1 FILE imp_place_test.ged +1 GEDC +2 VERS 5.5.1 +2 FORM LINEAGE-LINKED +1 CHAR UTF-8 +1 SUBM @SUBM1@ +0 @SUBM1@ SUBM +1 NAME The Gramps Tester +1 ADDR 112 Main St., Open Source, Software, Is Great +2 ADR1 112 Main St. +2 ADR2 Open Source +2 CITY Software +2 CTRY Is Great +2 POST 11111 +1 EMAIL gramps-users@@lists.sourceforge.net +1 FAX 9-999-999-9999 +1 LANG MyLang +1 PHON 8-888-888-8888 +1 WWW http://gramps-project.org +0 @I310@ INDI +1 NAME Living +1 SEX M +1 _UID B9C4D3F0D254674AA1B9023745BEBFE551C1 +1 CHAN +2 DATE 26 JAN 2015 +1 RESI +2 DATE 1960 +2 PLAC the place unknown +2 ADDR the address unknown +3 NOTE address enclosed by place, both new +1 RESI +2 DATE 1961 +2 ADDR the address unknown +3 NOTE address enclosed by place, both existing note also merged into existing place +3 SOUR @S1@ +4 PAGE Citation for 'the address unknown' +2 PLAC the place unknown +3 NOTE check that this note is retained when the place is merged into existing place +3 SOUR @S1@ +4 PAGE Citation for 'the place unknown' +1 RESI +2 DATE 1962 +2 PLAC 1962, the place +3 FORM Number, Unknown +3 NOTE setup the place +1 RESI +2 DATE 1963 +2 PLAC the place +2 ADDR 1963 address +3 ADR1 1963 address +3 ADR2 Mylocality +3 ADR3 Mytown +3 CITY Mytown +3 STAE Mystate +3 CTRY USA +3 NOTE the ADDR is new, but PLAC already exists, we skip enclosure for ADDR portion +1 RESI +2 DATE 1964 +2 PLAC Full 1964 place, Mylocality, Mytown, Mystate, USA, 12345 +3 FORM street, locality, city, state, country, zipcode +2 ADDR Addr detail on Full 1964 place +3 NOTE the Mylocality place already exists, other stuff new +1 RESI +2 DATE 1965 +2 ADDR raw address +1 RESI +2 DATE 1966 +2 ADDR Raw address in Mytown +3 CITY Mytown +3 STAE Mystate +3 CTRY USA +3 NOTE New raw address, enclosed by Mytown +1 RESI +2 DATE 1967 +2 PLAC the place +2 PLAC second place ignored +2 ADDR the address +2 ADDR second address ignored +3 NOTE second address ignored +0 @S1@ SOUR +1 AUTH The Gramps Test guys +1 PUBL 2017 +1 REPO @REPO1@ +1 TITL The Gramps Test Library +0 @REPO1@ REPO +1 NAME Gramps GitHub Library + + +0 TRLR diff --git a/data/tests/imp_place_test.gramps b/data/tests/imp_place_test.gramps new file mode 100644 index 00000000000..5b18966ae8f --- /dev/null +++ b/data/tests/imp_place_test.gramps @@ -0,0 +1,225 @@ + + + +
+ + + The Gramps Tester + 112 Main St., Open Source + Software + Is Great + 11111 + 8-888-888-8888 + gramps-users@lists.sourceforge.net + +
+ + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + + + M + + Living + + + + + + + + + + + + + + + + Citation for 'the address unknown' + 2 + + + + Citation for 'the place unknown' + 2 + + + + + + The Gramps Test Library + The Gramps Test guys + 2017 + + + + + + the place unknown + + + + + + the address unknown + + + + + + + + the place + + + + 1962, the place + + + + + + USA + + + + Mystate, USA + + + + + Mytown, Mystate, USA + + + + + 1963 address, Mylocality, Mytown, Mystate, USA + + + + + + + Mylocality, Mytown, Mystate, USA + + + + + Full 1964 place, Mylocality, Mytown, Mystate, USA + 12345 + + + + + Addr detail on Full 1964 place + + + + + + raw address + + + + Raw address in Mytown, Mytown, Mystate, USA + + + + + + the address + + + + + + + Gramps GitHub Library + Library + + + + + Records not imported into SUBM (Submitter): (@SUBM1@) The Gramps Tester: + +Line ignored as not understood Line 28: 1 LANG MyLang + + + + + address enclosed by place, both new + + + address enclosed by place, both existing note also merged into existing place + + + check that this note is retained when the place is merged into existing place + + + setup the place + + + the ADDR is new, but PLAC already exists, we skip enclosure for ADDR portion + + + the Mylocality place already exists, other stuff new + + + New raw address, enclosed by Mytown + + + Records not imported into INDI (individual) Gramps ID I0310: + +A second PLAC ignored Line 87: 2 PLAC second place ignored +Line ignored as not understood Line 89: 2 ADDR second address ignored +Skipped subordinate line Line 90: 3 NOTE second address ignored + + + + +
diff --git a/data/tests/imp_sample.ged b/data/tests/imp_sample.ged index 1afc41cff20..deb76581f4a 100644 --- a/data/tests/imp_sample.ged +++ b/data/tests/imp_sample.ged @@ -201,9 +201,8 @@ 2 PLAC San Francisco, San Francisco Co., CA 1 RESI 2 DATE 1980 -2 ADDR 459 Main St., The Village, San Francisco, CA, USA -3 ADR1 456 Main St -3 ADR1 456 Main St again +2 ADDR 456 Main St., The Village, San Francisco, CA, USA +3 ADR1 456 Main St. 3 ADR2 The Village 3 CITY San Francisco 3 CTRY USA diff --git a/data/tests/imp_sample.gramps b/data/tests/imp_sample.gramps index 9f4b108e0b8..4d5ae366455 100644 --- a/data/tests/imp_sample.gramps +++ b/data/tests/imp_sample.gramps @@ -6,7 +6,7 @@ Alex Roitman,,, - Not Provided + Not Provided, Not Provided @@ -102,447 +102,446 @@ Residence - + - + Birth Birth of Lillie Harriet Jones - + Death Death of Lillie Harriet Jones - + Birth Birth of John Hjalmar Smith - + Birth Birth of Eric Lloyd Smith - + Adopted - + Birth - + Birth of Amber Marie Smith - + Christening - + Christening of Amber Marie Smith - + Birth Birth of Carl Emil Smith - + Death Death of Carl Emil Smith - + Birth Birth of Hjalmar Smith - + Birth - No Date Information - + Death Death of Hjalmar Smith - + Death - + Nobility Title Sir Jimmy Smith - + Birth Birth of Martin Smith - + Death - + Death of Martin Smith - + Baptism Baptism of Martin Smith - + DATE 2007-12-21 - + Birth Birth of Astrid Shermanna Augusta Smith - + Death Death of Astrid Shermanna Augusta Smith - + Birth - + Birth of Gustaf Smith, Sr. - + Death Death of Gustaf Smith, Sr. - + Immi - + - + Christening Christening of Gustaf Smith, Sr. - + Birth - + Birth of Marta Ericsdotter - + Birth Birth of Kirsti Marie Smith - + Death Death of Kirsti Marie Smith - + Birth - + Birth of Ingeman Smith - + Birth - + Birth of Anna Streiffert - + Death Death of Anna Streiffert - + Birth Birth of Craig Peter Smith - + Census Census of Craig Peter Smith - + - + Birth - + Birth of Magnes Smith - + Death Death of Magnes Smith - + Birth - + Birth of Janice Ann Adams - + - + Occupation Retail Manager - + Degree Business Management - + Birth - + Birth of Marjorie Ohman - + Death Death of Marjorie Ohman - + Birth - + Birth of Darcy Horne - + Birth Birth of Lloyd Smith - + Birth Birth of Alice Paula Perkins - + Birth - + Birth of Lars Peter Smith - + Adopted - + Birth Birth of Elna Jefferson - + Death - + Death of Elna Jefferson - + Christening Christening of Elna Jefferson - + Birth - + Birth of Edwin Michael Smith - + - + Occupation Software Engineer - + - + Education - + Education of Edwin Michael Smith - + Degree B.S.E.E. - + Birth - + Birth of Kerstina Hansdotter - + Death - + Death of Kerstina Hansdotter - + Birth - + Birth of Martin Smith - + Death - + Death of Martin Smith - + Birth Birth of Ingeman Smith - + Birth - + Birth of Marjorie Alice Smith - + Birth Birth of Janis Elaine Green - + Birth - + Birth of Mason Michael Smith - + Christening - + Christening of Mason Michael Smith - + Birth Birth of Edwin Willard - + Birth Birth of Ingar Smith - + Birth Birth of Hjalmar Smith - + Death Death of Hjalmar Smith - + Baptism - + Baptism of Hjalmar Smith - + Immi - + - + Birth - + Birth of Emil Smith - + Residence - + At the boarding school - + Residence - + At the hebrew boarding school - + Marriage Marriage of Martin Smith and Elna Jefferson - + Marriage - + Marriage of Ingeman Smith and Marta Ericsdotter - + Marriage - + Marriage of Eric Lloyd Smith and Darcy Horne - + Marriage Marriage of Magnes Smith and Anna Streiffert - + - + Marriage Banns Celebration - + Marriage Marriage of John Hjalmar Smith and Alice Paula Perkins - + - + Marriage - + Marriage of Edwin Michael Smith and Janice Ann Adams - + Engagement @@ -552,44 +551,44 @@ - + Civil Common law marriage - + Marriage Marriage of Martin Smith and Kerstina Hansdotter - + Marriage Marriage of Edwin Willard and Kirsti Marie Smith - + Marriage Marriage of Herman Julius Nielsen and Astrid Shermanna Augusta Smith - + Marriage Marriage of Hjalmar Smith and Marjorie Ohman - + Marriage Marriage of Gus Smith and Evelyn Michaels - + Marriage Marriage of Lloyd Smith and Janis Elaine Green - + Marriage @@ -601,17 +600,17 @@ Smith Sr. - - + - + + - + - - + + F @@ -638,10 +637,10 @@ Kirsti Marie Smith - - + + - + F @@ -659,8 +658,8 @@ Smith Dr. - - + + @@ -677,13 +676,13 @@ Jimmy Smith - - + + - + M @@ -691,13 +690,13 @@ Hjalmar Smith - - + + - + M @@ -716,8 +715,8 @@ Carl Emil Smith - - + + @@ -797,71 +796,70 @@ - - + F Lillie Harriet Jones - - + + - + M John Hjalmar Smith - - + + - - + + - - + + - + M Eric Lloyd Smith - - + + - + - + F Amber Marie Smith - - + + - + M Martin Smith - - + - + + - - + + - + F Marta @@ -871,7 +869,7 @@ Marta Smith - + @@ -883,103 +881,103 @@ - - + + - + - + M Ingeman Smith - - + + - + F Anna Streiffert - - + + - + M Craig Peter Smith - - + + - + U Magnes Smith - + - - + + - + F Janice Ann Adams - - - - + + + + - + F Marjorie Ohman - + - + F Darcy Horne - - + + - + M Lloyd Smith - + - + F Alice Paula Perkins - - + + - + M Lars Peter @@ -992,123 +990,123 @@ Pete Jones - - + + - + F Elna Jefferson - - + + - + M Edwin Michael Smith - + - - + + - - - + + + - + F Kerstina Hansdotter - - + + - + M Martin Smith - - + + - + M Ingeman Smith - + - + F Marjorie Alice Smith - - + + - + F Janis Elaine Green - + - + M Mason Michael Smith - - - + + + - + M Edwin Willard - - + + - + F Ingar Smith - + - + M Emil Smith - - + - + + @@ -1130,201 +1128,201 @@ - - - - + + + + - - + + - - - + + + - + - - - + + + - - - + + + - + - + - - - + + + - + - - - + + + - + - - - + + + - + - + - - - - + + + + - + - + - - - - - - + + + + + + - + - - - - - + + + + + - + - - - + + + - + - + - + - + - + - - - - - + + + + + - + SSN docs pg 999 2 - + - + 2 - + - + 2 - + - + 2 - + - + 2 - - + + - + 2 - + - + 2 - + - + 2 - + - + 2 - + - + - + @S999@ - + Birth Records - - + + - + Marriage Certificae - - + + - + Birth Certificate - - - + + + - + Birth, Death and Marriage Records goodstuff - - + + - + No title - ID S0004 - + A weird source (one line) format @@ -1332,8 +1330,8 @@ Rønne, Bornholm, Denmark - - + + Löderup, Malmöhus Län, Sweden @@ -1355,96 +1353,106 @@ Reno, Washoe Co., NV - - 456 Main St again, The Village, San Francisco, CA, USA - - + + USA + + + + CA, USA + + + + + San Francisco, CA, USA + + - + + 456 Main St., The Village, San Francisco, CA, USA + + + + Hayward, Alameda Co., CA - + Community Presbyterian Church, Danville, CA - + Sweden - + Grostorp, Kristianstad Län, Sweden - + Copenhagen, Denmark - + Sweden - + Hoya/Jona/Hoia, Sweden - + Simrishamn, Kristianstad Län, Sweden - + Fremont, Alameda Co., CA - + Denver, Denver Co., CO - + Sacramento, Sacramento Co., CA - + Santa Rosa, Sonoma Co., CA - + San Jose, Santa Clara Co., CA - + UC Berkeley - + Smestorp, Kristianstad Län, Sweden - + Tommarp, Kristianstad Län, Sweden - + Rønne Bornholm, Denmark - + Oronoko, Berrien, Michigan, USA - + Shemps - + Woodland, Yolo Co., CA - - Sparks, Washoe Co., NV - - - + San Ramon, Conta Costa Co., CA @@ -1455,20 +1463,20 @@
- + - - + + - - + + - - + + - + New York Public Library Library
@@ -1479,29 +1487,28 @@ 11111
- + Invalid REPO Name Library - + Library - + Aunt Martha's Attic Library
- 123 Main St - LittleVillage + 123 Main St, LittleVillage Someville ST USA - - + +
- +
@@ -1544,7 +1551,7 @@ Filename omitted Line 48: Unknown, created to replace a missing note object. - @@ -1563,149 +1570,139 @@ Filename omitted Line 48: Evelyn Michaels N0007 - - Records not imported into INDI (individual) Gramps ID I0016: - -Warn: ADDR overwritten Line 206: 3 ADR1 456 Main St again -ADDR element ignored '459 Main St.' Line 204: 2 ADDR 459 Main St., The Village, San Francisco, CA, USA - - - - + Person Attribute Note on SSN - + Records not imported into INDI (individual) Gramps ID I0018: -Tag recognized but not supported Line 247: 2 TYPE first generaton +Tag recognized but not supported Line 244: 2 TYPE first generaton - + BIOGRAPHY Martin was listed as being a Husman, (owning a house as opposed to a farm) in the house records of Gladsax. - + A FAMC note - + Witness name: John Doe Witness comment: This is a simple test. - + Record for Edwin Michael Smith - + Witness name: No Name - + BIOGRAPHY Hjalmar sailed from Copenhagen, Denmark on the OSCAR II, 14 November 1912 arriving in New York 27 November 1912. He was seventeen years old. On the ship passenger list his trade was listed as a Blacksmith. He came to Reno, Nevada and lived with his sister Marie for a time before settling in Sparks. He worked for Southern Pacific Railroad as a car inspector for a time, then went to work for Standard Oil Company. He enlisted in the army at Sparks 7 December 1917 and served as a Corporal in the Medical Corp until his discharge 12 August 1919 at the Presidio in San Francisco, California. Both he and Marjorie are buried in the Masonic Memorial Gardens Mausoleum in Reno, he the 30th June 1975, and she the 25th of June 1980. - + Records not imported into FAM (family) Gramps ID F0010: -Tag recognized but not supported Line 865: 2 _STAT +Tag recognized but not supported Line 862: 2 _STAT - + Records not imported into FAM (family) Gramps ID F0011: -Could not import Magnes&Anna_smiths_marr_cert.jpg Line 880: 3 OBJE -Could not import Magnes&Anna_smiths_marr_cert.jpg Line 883: 2 OBJE +Could not import Magnes&Anna_smiths_marr_cert.jpg Line 877: 3 OBJE +Could not import Magnes&Anna_smiths_marr_cert.jpg Line 880: 2 OBJE - + Records not imported into FAM (family) Gramps ID F0012: -Could not import John&Alice_smiths_marr_cert.jpg Line 907: 1 OBJE +Could not import John&Alice_smiths_marr_cert.jpg Line 904: 1 OBJE - + Records not imported into FAM (family) Gramps ID F0008: -Tag recognized but not supported Line 1007: 1 ADDR 123 Main st, Grantville, Virginia, USA +Tag recognized but not supported Line 1004: 1 ADDR 123 Main st, Grantville, Virginia, USA - + But Aunt Martha still keeps the original! - + Invalid REPO (Name instead of xref) - + Source text of Birth cert - + Invalid REPO (no name) - + The repository reference from the source is important - + Records not imported into SOUR (source) Gramps ID S0003: -Tag recognized but not supported Line 1047: 1 DATA -Skipped subordinate line Line 1048: 2 AGNC NYC Public Library +Tag recognized but not supported Line 1044: 1 DATA +Skipped subordinate line Line 1045: 2 AGNC NYC Public Library - + A note on this address - + Some note on the repo - + Records not imported into REPO (repository) Gramps ID R0003: -REFN ignored Line 1077: 3 REFN blah blah -Skipped subordinate line Line 1078: 4 TYPE who knows -Could not import Attic_photo.jpg Line 1081: 3 OBJE +REFN ignored Line 1074: 3 REFN blah blah +Skipped subordinate line Line 1075: 4 TYPE who knows +Could not import Attic_photo.jpg Line 1078: 3 OBJE - + Records not imported into Top Level: -Unknown tag Line 1108: 0 XXX an unknown token at level 0 +Unknown tag Line 1105: 0 XXX an unknown token at level 0 - + Records not imported into Top Level: -Unknown tag Line 1111: 1 @X1@ XXX and unknown token xref definition +Unknown tag Line 1108: 1 @X1@ XXX and unknown token xref definition - + Objects referenced by this note were missing in a file imported on 12/25/1999 12:00:00 AM. diff --git a/gramps/plugins/lib/libgedcom.py b/gramps/plugins/lib/libgedcom.py index aabd2a73acb..5fbe092f277 100644 --- a/gramps/plugins/lib/libgedcom.py +++ b/gramps/plugins/lib/libgedcom.py @@ -130,11 +130,8 @@ from gramps.gen.utils.unknown import make_unknown, create_explanation_note from gramps.gen.datehandler._dateparser import DateParser from gramps.gen.db.dbconst import EVENT_KEY -from gramps.gen.lib.const import IDENTICAL from gramps.gen.lib import (StyledText, StyledTextTag, StyledTextTagType) from gramps.gen.lib.urlbase import UrlBase -from gramps.plugins.lib.libplaceimport import PlaceImport -from gramps.gen.display.place import displayer as _pd from gramps.gen.utils.grampslocale import GrampsLocale #------------------------------------------------------------------------- @@ -275,6 +272,7 @@ TOKEN__JUST = 135 TOKEN__TEXT = 136 TOKEN__DATE = 137 +TOKEN_ADR3 = 138 TOKENS = { "_ADPN" : TOKEN__ADPN, @@ -335,6 +333,7 @@ "ADOPT" : TOKEN_ADOP, "ADR1" : TOKEN_ADR1, "ADR2" : TOKEN_ADR2, + "ADR3" : TOKEN_ADR3, "AFN" : TOKEN_AFN, "AGE" : TOKEN_AGE, "AGENCY" : TOKEN_IGNORE, @@ -1582,13 +1581,15 @@ def __init__(self, person=None, level=0, event=None, event_ref=None): self.primary = False # _PRIMARY tag on an INDI.FAMC tag self.filename = "" self.title = "" - self.addr = None - self.res = None + self.place = None + self.place_pf = None # PLAC.FORM parser + self.place_fields = None # method for parsing places + self.addr = None # Hold ADDR structure + self.addr_place = None # The Place that will hold the ADDR data + self.addr_pf = None # the FORM for the ADDR + self.res = None # Hold Researcher structure self.source = None self.ftype = None - self.pf = None # method for parsing places - self.location = None - self.place_fields = None # method for parsing places self.ref = None # PersonRef self.handle = None # self.form = "" # Multimedia format @@ -1600,7 +1601,6 @@ def __init__(self, person=None, level=0, event=None, event_ref=None): self.name = "" self.ignore = False self.repo_ref = None - self.place = None self.media = None self.photo = "" # Person primary photo self.prim = None # Photo is primary @@ -1626,92 +1626,62 @@ def __setattr__(self, name, value): class PlaceParser: """ Provide the ability to parse GEDCOM FORM statements for places, and - the parse the line of text, mapping the text components to Location - values based of the FORM statement. + mapping the text components to PlaceType values based on the FORM + statement. + + Note that postal codes are a special case. + + For now this assumes FORM statements are in English or the local language; + a questionable assumption. If not recognized, they will be treated as + CUSTOM grampstypes. """ + POSTAL = -99 __field_map = { - 'addr' : Location.set_street, - 'subdivision' : Location.set_street, - 'addr1' : Location.set_street, - 'adr1' : Location.set_street, - 'street' : Location.set_street, - 'addr2' : Location.set_locality, - 'adr2' : Location.set_locality, - 'locality' : Location.set_locality, - 'neighborhood' : Location.set_locality, - 'city' : Location.set_city, - 'town' : Location.set_city, - 'village' : Location.set_city, - 'county' : Location.set_county, - 'country' : Location.set_country, - 'state' : Location.set_state, - 'state/province': Location.set_state, - 'region' : Location.set_state, - 'province' : Location.set_state, - 'area code' : Location.set_postal_code, - 'post code' : Location.set_postal_code, - 'zip code' : Location.set_postal_code, } + 'Addr' : PlaceType.STREET, + 'Subdivision' : PlaceType.STREET, + 'Addr1' : PlaceType.STREET, + 'Adr1' : PlaceType.STREET, + 'Addr2' : PlaceType.LOCALITY, + 'Adr2' : PlaceType.LOCALITY, + 'State/province': PlaceType.STATE, + 'Area code' : POSTAL, + 'Post code' : POSTAL, + 'Postal' : POSTAL, + 'Zipcode' : POSTAL, + 'Zip code' : POSTAL, } def __init__(self, line=None): - self.parse_function = [] + self.pf_list = [] if line: self.parse_form(line) def parse_form(self, line): """ - Parses the GEDCOM PLAC.FORM into a list of function - pointers (if possible). It does this my mapping the text strings - (separated by commas) to the corresponding Location - method via the __field_map variable + Parses the GEDCOM PLAC.FORM into a list of PlaceTypes(if possible). + It does this my mapping the text strings (separated by commas) to the + corresponding PlaceType by trying our own __field_map, then the + translated PlaceType set, then the English PlaceType set. """ + self.pf_list = [] for item in line.data.split(','): - item = item.lower().strip() - fcn = self.__field_map.get(item, lambda x, y: None) - self.parse_function.append(fcn) - - def load_place(self, place_import, place, text): - """ - Takes the text string representing a place, splits it into - its subcomponents (comma separated), and calls the approriate - function based of its position, depending on the parsed value - from the FORM statement. - """ - items = [item.strip() for item in text.split(',')] - if len(items) != len(self.parse_function): - return - index = 0 - loc = Location() - for item in items: - self.parse_function[index](loc, item) - index += 1 - - location = (loc.get_street(), - loc.get_locality(), - loc.get_parish(), - loc.get_city(), - loc.get_county(), - loc.get_state(), - loc.get_country()) - - for level, name in enumerate(location): - if name: - break + item = item.strip().capitalize() + type_num = self.__field_map.get(item, None) + if type_num == self.POSTAL: + pass + elif type_num is not None: + type_num = PlaceType(type_num) + else: + type_num = PlaceType(item) + if type_num.is_custom(): + type_num = PlaceType() + type_num.set_from_xml_str(item) + self.pf_list.append(type_num) - if name: - type_num = 7 - level - else: - name = place.title - type_num = PlaceType.UNKNOWN - place.name.set_value(name) - place.set_type(PlaceType(type_num)) - code = loc.get_postal_code() - place.set_code(code) - if place.handle: # if handle is available, store immediately - place_import.store_location(location, place.handle) - else: # return for storage later - return location + def get_pf(self): + """ return the list of PlaceTypes associated with the PLAC.FORM """ + return self.pf_list #------------------------------------------------------------------------- @@ -1754,7 +1724,7 @@ def find_next(self): # #------------------------------------------------------------------------- class IdMapper: - """ This class provide methods to keep track of the correspoindence between + """ This class provide methods to keep track of the correspondence between Gedcom xrefs (@P1023@) and Gramps IDs. """ def __init__(self, has_gid, find_next, id2user_format): self.has_gid = has_gid @@ -1825,7 +1795,7 @@ class GedcomParser(UpdateCallback): BadFile = "Not a GEDCOM file" @staticmethod - def __find_from_handle(gramps_id, table): + def __find_hndl_from_id(gramps_id, table): """ Find a handle corresponding to the specified Gramps ID. @@ -1870,6 +1840,19 @@ def __parse_name_personal(text): name.set_first_name(text.strip()) return name + @staticmethod + def __add_placeref(place, ref_hndl, date=None): + """ + Adds a PlaceRef to a Place. It checks for duplicates before adding. + """ + pref = PlaceRef() + pref.ref = ref_hndl + pref.date = date + for ref in place.placeref_list: + if ref.serialize() == pref.serialize(): + return + place.add_placeref(pref) + def __init__(self, dbase, ifile, filename, user, stage_one, default_source, default_tag_format=None): UpdateCallback.__init__(self, user.callback) @@ -1943,16 +1926,13 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.dbase.find_next_note_gramps_id, self.dbase.nid2user_format) - self.gid2id = {} - self.oid2id = {} - self.sid2id = {} - self.lid2id = {} - self.fid2id = {} - self.rid2id = {} - self.nid2id = {} - - self.place_import = PlaceImport(self.dbase) - +# Despite the name, the following are Gramps ID to handle + self.gid2id = {} # Person + self.fid2id = {} # Family + self.sid2id = {} # Source + self.oid2id = {} # Media + self.rid2id = {} # Repo + self.nid2id = {} # Note # # Parse table for <> below the level 0 SUBM tag # @@ -2319,25 +2299,6 @@ def __init__(self, dbase, ifile, filename, user, stage_one, } self.func_list.append(self.media_parse_tbl) - self.parse_loc_tbl = { - TOKEN_ADR1 : self.__location_adr1, - TOKEN_ADR2 : self.__location_adr2, - TOKEN_CITY : self.__location_city, - TOKEN_STAE : self.__location_stae, - TOKEN_POST : self.__location_post, - TOKEN_CTRY : self.__location_ctry, - # Not legal GEDCOM - not clear why these are included at this level - TOKEN_ADDR : self.__ignore, - TOKEN_DATE : self.__ignore, # there is nowhere to put a date - TOKEN_NOTE : self.__location_note, - TOKEN_RNOTE : self.__location_note, - TOKEN__LOC : self.__ignore, - TOKEN__NAME : self.__ignore, - TOKEN_PHON : self.__location_phone, - TOKEN_IGNORE : self.__ignore, - } - self.func_list.append(self.parse_loc_tbl) - # # Parse table for <> below the level 0 FAM tag # @@ -2506,13 +2467,14 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.parse_addr_tbl = { TOKEN_DATE : self.__address_date, - TOKEN_ADR1 : self.__address_adr1, - TOKEN_ADR2 : self.__address_adr2, + TOKEN_ADR1 : self.__address_adr, + TOKEN_ADR2 : self.__address_adr, + TOKEN_ADR3 : self.__address_adr, TOKEN_CITY : self.__address_city, TOKEN_STAE : self.__address_state, TOKEN_POST : self.__address_post, TOKEN_CTRY : self.__address_country, - TOKEN_PHON : self.__ignore, + TOKEN_PHON : self.__address_phone, TOKEN_SOUR : self.__address_sour, TOKEN_NOTE : self.__address_note, TOKEN_RNOTE : self.__address_note, @@ -2530,21 +2492,30 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.func_list.append(self.event_cause_tbl) self.event_place_map = { - TOKEN_NOTE : self.__event_place_note, - TOKEN_RNOTE : self.__event_place_note, - TOKEN_FORM : self.__event_place_form, + # +1 << NOTE_STRUCTURE >> {0:M} + TOKEN_NOTE : self.__place_note, + TOKEN_RNOTE : self.__place_note, + # +1 FORM {0:1} + TOKEN_FORM : self.__place_form, + # +1 FONE {0:M} + # +2 TYPE {1:1} + # +1 ROMN {0:M} + # +2 TYPE {1:1} + # +1 MAP {0:1} + TOKEN_MAP : self.__place_map, # self.place_map_tbl # Not legal. - TOKEN_OBJE : self.__event_place_object, - TOKEN_SOUR : self.__event_place_sour, + TOKEN_OBJE : self.__place_object, + TOKEN_SOUR : self.__place_sour, TOKEN__LOC : self.__ignore, - TOKEN_MAP : self.__place_map, # Not legal, but generated by Ultimate Family Tree TOKEN_QUAY : self.__ignore, } self.func_list.append(self.event_place_map) self.place_map_tbl = { + # +1 LATI {1:1} TOKEN_LATI : self.__place_lati, + # +1 LONG {1:1} TOKEN_LONG : self.__place_long, } self.func_list.append(self.place_map_tbl) @@ -2667,7 +2638,7 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.func_list.append(self.header_subm) self.place_form = { - TOKEN_FORM : self.__place_form, + TOKEN_FORM : self.__header_place_form, } self.func_list.append(self.place_form) @@ -2759,8 +2730,6 @@ def parse_gedcom_file(self, use_trans=False): self.dbase.add_source(src, self.trans) self.__clean_up() - self.place_import.generate_hierarchy(self.trans) - if not self.dbase.get_feature("skip-check-xref"): self.__check_xref() self.dbase.enable_signals() @@ -2794,25 +2763,25 @@ def __find_person_handle(self, gramps_id): """ Return the database handle associated with the person's Gramps ID """ - return self.__find_from_handle(gramps_id, self.gid2id) + return self.__find_hndl_from_id(gramps_id, self.gid2id) def __find_family_handle(self, gramps_id): """ Return the database handle associated with the family's Gramps ID """ - return self.__find_from_handle(gramps_id, self.fid2id) + return self.__find_hndl_from_id(gramps_id, self.fid2id) def __find_media_handle(self, gramps_id): """ Return the database handle associated with the media object's Gramps ID """ - return self.__find_from_handle(gramps_id, self.oid2id) + return self.__find_hndl_from_id(gramps_id, self.oid2id) def __find_note_handle(self, gramps_id): """ Return the database handle associated with the media object's Gramps ID """ - return self.__find_from_handle(gramps_id, self.nid2id) + return self.__find_hndl_from_id(gramps_id, self.nid2id) def __find_or_create_person(self, gramps_id): """ @@ -2825,7 +2794,7 @@ def __find_or_create_person(self, gramps_id): if self.dbase.has_person_handle(intid): person.unserialize(self.dbase.get_raw_person_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.gid2id) + intid = self.__find_hndl_from_id(gramps_id, self.gid2id) person.set_handle(intid) person.set_gramps_id(gramps_id) return person @@ -2843,7 +2812,7 @@ def __find_or_create_family(self, gramps_id): if self.dbase.has_family_handle(intid): family.unserialize(self.dbase.get_raw_family_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.fid2id) + intid = self.__find_hndl_from_id(gramps_id, self.fid2id) family.set_handle(intid) family.set_gramps_id(gramps_id) return family @@ -2859,7 +2828,7 @@ def __find_or_create_media(self, gramps_id): if self.dbase.has_media_handle(intid): obj.unserialize(self.dbase.get_raw_media_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.oid2id) + intid = self.__find_hndl_from_id(gramps_id, self.oid2id) obj.set_handle(intid) obj.set_gramps_id(gramps_id) return obj @@ -2877,7 +2846,7 @@ def __find_or_create_source(self, gramps_id): if self.dbase.has_source_handle(intid): obj.unserialize(self.dbase.get_raw_source_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.sid2id) + intid = self.__find_hndl_from_id(gramps_id, self.sid2id) obj.set_handle(intid) obj.set_gramps_id(gramps_id) return obj @@ -2896,7 +2865,7 @@ def __find_or_create_repository(self, gramps_id): if self.dbase.has_repository_handle(intid): repository.unserialize(self.dbase.get_raw_repository_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.rid2id) + intid = self.__find_hndl_from_id(gramps_id, self.rid2id) repository.set_handle(intid) repository.set_gramps_id(gramps_id) return repository @@ -2920,7 +2889,7 @@ def __find_or_create_note(self, gramps_id): if self.dbase.has_note_handle(intid): note.unserialize(self.dbase.get_raw_note_data(intid)) else: - intid = self.__find_from_handle(gramps_id, self.nid2id) + intid = self.__find_hndl_from_id(gramps_id, self.nid2id) note.set_handle(intid) note.set_gramps_id(gramps_id) if need_commit: @@ -2943,35 +2912,32 @@ def __loc_is_empty(self, location): return True return False - def __find_place(self, title, location, placeref_list): + def __find_place(self, title, ptype, pref): """ - Finds an existing place based on the title and primary location. + Finds an existing place based on the title, place type and placeref. @param title: The place title @type title: string - @param location: The current location - @type location: gen.lib.Location + @param ptype: The place type + @type ptype: PlaceType + @param pref: The PlaceRef.ref handle + @type pref: Handle @return gen.lib.Place """ for place_handle in self.place_names[title]: place = self.dbase.get_place_from_handle(place_handle) - if place.get_title() == title: - if self.__loc_is_empty(location) and \ - self.__loc_is_empty(self.__get_first_loc(place)) and \ - place.get_placeref_list() == placeref_list: - return place - elif (not self.__loc_is_empty(location) and - not self.__loc_is_empty(self.__get_first_loc(place)) and - self.__get_first_loc(place).is_equivalent(location) == - IDENTICAL) and \ - place.get_placeref_list() == placeref_list: + if place.get_title() == title and place.get_type() == ptype: + if not pref and place.get_placeref_list() == []: return place + for placeref in place.get_placeref_list(): + if placeref.ref == pref: + return place return None def __add_place(self, event, sub_state): """ - Add a new place to an event if not already present, or update a - place. + Add a place to an event, make new place if not already present, or + update an existing place. @param event: The event @type event: gen.lib.Event @@ -2979,32 +2945,115 @@ def __add_place(self, event, sub_state): by event_parse_tbl) @type sub_state: CurrentState """ + place = None if sub_state.place: - # see whether this place already exists - place = self.__find_place(sub_state.place.get_title(), - self.__get_first_loc(sub_state.place), - sub_state.place.get_placeref_list()) - if place is None: - place = sub_state.place - place_title = _pd.display(self.dbase, place) - location = sub_state.pf.load_place(self.place_import, place, - place_title) - self.dbase.add_place(place, self.trans) - # if 'location was created, then store it, now that we have a - # handle. - if location: - self.place_import.store_location(location, place.handle) - self.place_names[place.get_title()].append(place.get_handle()) - event.set_place_handle(place.get_handle()) + # we have a place + ptypes = sub_state.place_pf.get_pf() + places = sub_state.place.get_title().split(',') + if len(places) == len(ptypes): + # we have a hierarchy, go through it largest first + prev_place_hndl = None + title = '' + for indx in reversed(range(len(places))): + name = places[indx].strip() + if not name: + continue + if ptypes[indx] == PlaceParser.POSTAL: + sub_state.place.set_code(name) + continue + title = name + \ + ((', ' + title) if title else '') + place = self.__find_place(title, ptypes[indx], + prev_place_hndl) + if place is not None: + prev_place_hndl = place.get_handle() + if indx != 0: + # still checking the hierarchy + prev_place_hndl = place.get_handle() + continue + else: + # already have a place, need to merge in our stuff + # we leave title alone, but set name for merge, if + # different, it will end up in alt-names + sub_state.place.name.value = name + if prev_place_hndl: + self.__add_placeref(sub_state.place, + prev_place_hndl) + place.merge(sub_state.place) + self.dbase.commit_place(place, self.trans) + break + elif indx != 0: + # still making hierarchy, Create the place + place = Place() + else: + # a new place with all the items + place = sub_state.place + place.set_title(title) + place.name.value = name + if prev_place_hndl: + self.__add_placeref(place, prev_place_hndl) + place.set_type(ptypes[indx]) + self.dbase.add_place(place, self.trans) + prev_place_hndl = place.get_handle() + self.place_names[title].append(place.handle) else: - place.merge(sub_state.place) - place_title = _pd.display(self.dbase, place) - location = sub_state.pf.load_place(self.place_import, place, - place_title) - self.dbase.commit_place(place, self.trans) - if location: - self.place_import.store_location(location, place.handle) - event.set_place_handle(place.get_handle()) + # No hierarchy available, just save it. + sub_state.place.name.value = sub_state.place.title + place = self.__find_place(sub_state.place.title, + sub_state.place.place_type, None) + if place is None: + place = sub_state.place + self.dbase.add_place(place, self.trans) + self.place_names[place.title].append(place.handle) + else: + place.merge(sub_state.place) + self.dbase.commit_place(place, self.trans) + event.set_place_handle(place.get_handle()) + if sub_state.addr_place: + # ADDR was in EVEN, need to make hierarchy + ptypes = sub_state.addr_pf + places = sub_state.addr + # we have a hierarchy, go through it largest first + prev_place_hndl = None + title = '' + for indx in reversed(range(len(places))): + title = places[indx].strip() + \ + ((', ' + title) if title else '') + if indx == 0 and place and not prev_place_hndl: + prev_place_hndl = place.handle + a_place = self.__find_place(title, ptypes[indx], + prev_place_hndl) + if a_place is not None: + prev_place_hndl = a_place.get_handle() + if indx != 0: + # still checking the hierarchy + continue + else: + # already have a place, need to merge in our stuff + a_place.merge(sub_state.addr_place) + self.dbase.commit_place(a_place, self.trans) + continue + elif indx != 0: + # still making hierarchy, Create the place + a_place = Place() + else: + # a new place with all the items + a_place = sub_state.addr_place + a_place.set_title(title) + a_place.name.set_value(places[indx].strip()) + if prev_place_hndl: + self.__add_placeref(a_place, prev_place_hndl) + a_place.set_type(ptypes[indx]) + self.dbase.add_place(a_place, self.trans) + prev_place_hndl = a_place.get_handle() + self.place_names[title].append(a_place.get_handle()) + event.set_place_handle(a_place.get_handle()) + + if sub_state.addr_place and place: + # ADDR and PLAC were in EVEN, need to enclose Addr place in + # Place. + self.__add_placeref(a_place, place.get_handle()) + self.dbase.commit_place(a_place, self.trans) def __find_file(self, fullname, altpath): # try to find the media file @@ -3216,8 +3265,8 @@ def __check(_map, has_gid_func, class_func, commit_func, for input_id, gramps_id in _map.map().items(): # Check whether an object exists for the mapped gramps_id if not has_gid_func(gramps_id): - _handle = self.__find_from_handle(gramps_id, - gramps_id2handle) + _handle = self.__find_hndl_from_id(gramps_id, + gramps_id2handle) if msg == "FAM": make_unknown(gramps_id, self.explanation.handle, class_func, commit_func, self.trans, @@ -3268,7 +3317,7 @@ def __input_fid(gramps_id): return key for input_id, gramps_id in self.pid_map.map().items(): - person_handle = self.__find_from_handle(gramps_id, self.gid2id) + person_handle = self.__find_hndl_from_id(gramps_id, self.gid2id) person = self.dbase.get_person_from_handle(person_handle) for family_handle in person.get_family_handle_list(): family = self.dbase.get_family_from_handle(family_handle) @@ -3293,7 +3342,7 @@ def __input_pid(gramps_id): return key for input_id, gramps_id in self.fid_map.map().items(): - family_handle = self.__find_from_handle(gramps_id, self.fid2id) + family_handle = self.__find_hndl_from_id(gramps_id, self.fid2id) family = self.dbase.get_family_from_handle(family_handle) father_handle = family.get_father_handle() mother_handle = family.get_mother_handle() @@ -3358,19 +3407,20 @@ def __input_pid(gramps_id): "To correct for that, %(new)d objects were created and\n" "their typifying attribute was set to 'Unknown'.\n" "Where possible these 'Unknown' objects are \n" - "referenced by note %(unknown)s.\n" - ) % {'new': self.missing_references, - 'unknown': self.explanation.gramps_id} + "referenced by note %(unknown)s.\n") % \ + {'new': self.missing_references, + 'unknown': self.explanation.gramps_id} self.__add_msg(txt) self.number_of_errors -= 1 - def __merge_address(self, free_form_address, addr, line, state): + def __merge_address(self, addr, line): """ Merge freeform and structured addrssses. n ADDR {0:1} +1 CONT {0:M} +1 ADR1 {0:1} (Street) - +1 ADR2 {0:1} (Locality) + +1 ADR2 {0:1} + +1 ADR3 {0:1} +1 CITY {0:1} +1 STAE {0:1} +1 POST {0:1} @@ -3379,12 +3429,11 @@ def __merge_address(self, free_form_address, addr, line, state): This is done along the lines suggested by Tamura Jones in http://www.tamurajones.net/GEDCOMADDR.xhtml as a result of bug 6382. "When a GEDCOM reader encounters a double address, it should read the - structured address. ... A GEDCOM reader that does verify that the - addresses are the same should issue an error if they are not". + structured address. This is called for SUBMitter addresses (__subm_addr), INDIvidual addresses (__person_addr), REPO addresses and HEADer corp address - (__repo_address) and EVENt addresses (__event_adr). + (__repo_address). The structured address (if any) will have been accumulated into an object of type LocationBase, which will either be a Location, or an @@ -3397,48 +3446,65 @@ def __merge_address(self, free_form_address, addr, line, state): structured components. N.B. PAF provides a free-form address and a country, so this allows for that case. - If both forms of address are provided, then the structured address is - used, and if the ADDR/CONT contains anything not in the structured - address, a warning is issued. + If both forms of address are provided, then the Structured parts of + address are removed from the free-form version, anything left is put + back into the front of name/title which is rebuilt from structured + address. + TODO for Arabic, should the output commas be translated? If just ADR1, ADR2, CITY, STAE, POST or CTRY are provided (this is not actually legal GEDCOM symtax, but may be possible by GEDCOM extensions) then just the structrued address is used. - The routine returns a string suitable for a title. + The routine returns a string suitable for a title, or None if no useful + structured address is included. """ title = '' - free_form_address = free_form_address.replace('\n', ', ') - if not (addr.get_street() or addr.get_locality() or - addr.get_city() or addr.get_state() or - addr.get_postal_code()): - - addr.set_street(free_form_address) - return free_form_address - else: - # structured address provided - addr_list = free_form_address.split(",") - str_list = [] - for func in (addr.get_street(), addr.get_locality(), - addr.get_city(), addr.get_state(), - addr.get_postal_code(), addr.get_country()): - str_list += [i.strip(',' + string.whitespace) - for i in func.split("\n")] - for elmn in addr_list: - if elmn.strip(',' + string.whitespace) not in str_list: - # message means that the element %s was ignored, but - # expressed the wrong way round because the message is - # truncated for output - self.__add_msg(_("ADDR element ignored '%s'" - % elmn), line, state) - # The free-form address ADDR is discarded - # Assemble a title out of structured address - for elmn in str_list: - if elmn: - if title != '': - # TODO for Arabic, should the next comma be translated? - title += ', ' - title += elmn - return title + ff_addr = line.data.replace('\n', ', ') + if not (addr.get_street() or addr.get_city() or addr.get_state()): + addr.set_street(ff_addr) + return None + # structured address provided + # Since ADR1, ADR2, ADR3 form a second free-form address we will + # merge with the ff addr, removing duplicates and preserve order. + # This assumes that the order between the two is the same if there + # are common elements, although there may be extras in one or the + # other. + ff_list = [item.strip() for item in ff_addr.split(',') if item.strip()] + street = addr.get_street().replace('\n', ', ') + st_list = [item.strip() for item in street.split(',') if item.strip()] + mrg_list = [] + for item in ff_list: + if item in st_list: + while True: + item2 = st_list[0] + mrg_list.append(item2) + del st_list[0] + if item == item2: + break + else: + mrg_list.append(item) + mrg_list.extend(st_list) + + len_mrg_list = len(mrg_list) + for item in (addr.get_city(), addr.get_state(), + addr.get_postal_code(), addr.get_country()): + item = item.replace("\n", " ").strip(',' + string.whitespace) + indx = 0 + while indx < len_mrg_list: + if item == mrg_list[indx]: + del mrg_list[indx] + mrg_list.append(item) + len_mrg_list -= 1 + indx += 1 + # Reassemble a street from leftovers + for item in mrg_list: + if title != '': + title += ', ' + title += item + len_mrg_list -= 1 + if not len_mrg_list: + addr.set_street(title) + return title def __parse_trailer(self): """ @@ -3962,7 +4028,7 @@ def __person_std_event(self, line, state): sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4107,7 +4173,7 @@ def __person_addr(self, line, state): self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) state.msg += sub_state.msg - self.__merge_address(free_form, sub_state.addr, line, state) + self.__merge_address(sub_state.addr, line) state.person.add_address(sub_state.addr) def __person_resi(self, line, state): @@ -4207,7 +4273,7 @@ def __person_titl(self, line, state): sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4594,19 +4660,13 @@ def build_lds_ord(self, state, lds_type): sub_state.level = state.level + 1 sub_state.lds_ord = LdsOrd() sub_state.lds_ord.set_type(lds_type) - sub_state.place = None - sub_state.place_fields = PlaceParser() + sub_state.place_pf = PlaceParser() sub_state.person = state.person state.person.lds_ord_list.append(sub_state.lds_ord) self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) state.msg += sub_state.msg - - if sub_state.place: - place_title = _pd.display(self.dbase, sub_state.place) - sub_state.place_fields.load_place(self.place_import, - sub_state.place, - place_title) + self.__add_place(sub_state.lds_ord, sub_state) def __lds_temple(self, line, state): """ @@ -4654,7 +4714,7 @@ def __lds_form(self, line, state): @param state: The current state @type state: CurrentState """ - state.pf = PlaceParser(line) + state.place_pf = PlaceParser(line) def __lds_plac(self, line, state): """ @@ -4665,21 +4725,19 @@ def __lds_plac(self, line, state): @type line: GedLine @param state: The current state @type state: CurrentState + + The Gedcom spec doesn't treat LDS places the same as event places, + instead it just has the single line with title. """ - try: - title = line.data - place = self.__find_place(title, None, None) - if place is None: - place = Place() - place.set_title(title) - place.name.set_value(title) - self.dbase.add_place(place, self.trans) - self.place_names[place.get_title()].append(place.get_handle()) - else: - pass - state.lds_ord.set_place_handle(place.handle) - except NameError: - return + title = line.data + if state.place is None: + state.place = Place() + state.place.set_title(title) + state.place.name.set_value(line.data) + else: + # We have previously found a PLAC + self.__add_msg(_("A second PLAC ignored"), line, state) + # ignore this second PLAC, and use the old one def __lds_sour(self, line, state): """ @@ -4996,26 +5054,6 @@ def __parse_fam(self, line): self.__parse_level(state, self.family_func, self.__family_even) - # handle addresses attached to families - if state.addr is not None: - father_handle = family.get_father_handle() - father = self.dbase.get_person_from_handle(father_handle) - if father: - father.add_address(state.addr) - self.dbase.commit_person(father, self.trans) - mother_handle = family.get_mother_handle() - mother = self.dbase.get_person_from_handle(mother_handle) - if mother: - mother.add_address(state.addr) - self.dbase.commit_person(mother, self.trans) - - for child_ref in family.get_child_ref_list(): - child_handle = child_ref.ref - child = self.dbase.get_person_from_handle(child_handle) - if child: - child.add_address(state.addr) - self.dbase.commit_person(child, self.trans) - # add default reference if no reference exists self.__add_default_source(family) @@ -5078,7 +5116,7 @@ def __family_std_event(self, line, state): sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5135,7 +5173,7 @@ def __family_even(self, line, state): sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5219,17 +5257,13 @@ def __family_slgs(self, line, state): sub_state.lds_ord.set_type(LdsOrd.SEAL_TO_SPOUSE) sub_state.place = None sub_state.family = state.family - sub_state.place_fields = PlaceParser() + # LDS places don't use HEAD.PLAC.FORM + sub_state.place_pf = PlaceParser() state.family.lds_ord_list.append(sub_state.lds_ord) self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) state.msg += sub_state.msg - - if sub_state.place: - place_title = _pd.display(self.dbase, sub_state.place) - sub_state.place_fields.load_place(self.place_import, - sub_state.place, - place_title) + self.__add_place(sub_state.lds_ord, sub_state) def __family_source(self, line, state): """ @@ -5653,21 +5687,16 @@ def __event_place(self, line, state): else: place = state.place if place: - # We encounter a PLAC, having previously encountered an ADDR - if state.place.place_type.string != _("Address"): - # We have previously found a PLAC - self.__add_msg(_("A second PLAC ignored"), line, state) - # ignore this second PLAC, and use the old one - else: - # This is the first PLAC - place.set_title(line.data) - place.name.set_value(line.data) - else: - # The first thing we encounter is PLAC - state.place = Place() - place = state.place - place.set_title(line.data) - place.name.set_value(line.data) + # We have previously found a PLAC + self.__add_msg(_("A second PLAC ignored"), line, state) + # ignore this second PLAC + self.__skip_subordinate_levels(line.level + 1, state) + return + # The first thing we encounter is PLAC + state.place = Place() + place = state.place + place.set_title(line.data) + place.name.set_value(line.data) sub_state = CurrentState() sub_state.place = place @@ -5676,12 +5705,13 @@ def __event_place(self, line, state): self.__parse_level(sub_state, self.event_place_map, self.__undefined) state.msg += sub_state.msg - if sub_state.pf: # if we found local PLAC:FORM - state.pf = sub_state.pf # save to override global value + if sub_state.place_pf: # if we found local PLAC:FORM + # save to override global value + state.place_pf = sub_state.place_pf # merge notes etc into place state.place.merge(sub_state.place) - def __event_place_note(self, line, state): + def __place_note(self, line, state): """ @param line: The current line in GedLine format @type line: GedLine @@ -5690,16 +5720,16 @@ def __event_place_note(self, line, state): """ self.__parse_note(line, state.place, state) - def __event_place_form(self, line, state): + def __place_form(self, line, state): """ @param line: The current line in GedLine format @type line: GedLine @param state: The current state @type state: CurrentState """ - state.pf = PlaceParser(line) + state.place_pf = PlaceParser(line) - def __event_place_object(self, line, state): + def __place_object(self, line, state): """ @param line: The current line in GedLine format @type line: GedLine @@ -5708,7 +5738,7 @@ def __event_place_object(self, line, state): """ self.__obje(line, state, state.place) - def __event_place_sour(self, line, state): + def __place_sour(self, line, state): """ @param line: The current line in GedLine format @type line: GedLine @@ -5719,7 +5749,6 @@ def __event_place_sour(self, line, state): def __place_map(self, line, state): """ - n MAP n+1 LONG n+1 LATI @@ -5760,95 +5789,53 @@ def __event_addr(self, line, state): @type line: GedLine @param state: The current state @type state: CurrentState + + Process the ADDR record for events. The data is saved in state.addr + and state.addr_place for later processing and commit in __add_place. """ - free_form = line.data + if state.addr_place: + # only one ADDR allowed in the event. + self.__not_recognized(line, state) + return sub_state = CurrentState(level=state.level + 1) - sub_state.location = Location() - sub_state.event = state.event + sub_state.addr = addr = Address() sub_state.place = Place() # temp stash for notes, citations etc - self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) + self.__parse_level(sub_state, self.parse_addr_tbl, self.__undefined) state.msg += sub_state.msg - title = self.__merge_address(free_form, sub_state.location, - line, state) - - location = sub_state.location - - if self.addr_is_detail and state.place: - # Commit the enclosing place - place = self.__find_place(state.place.get_title(), None, - state.place.get_placeref_list()) - if place is None: - place = state.place - self.dbase.add_place(place, self.trans) - self.place_names[place.get_title()].append(place.get_handle()) - else: - place.merge(state.place) - self.dbase.commit_place(place, self.trans) - place_title = _pd.display(self.dbase, place) - state.pf.load_place(self.place_import, place, place_title) - - # Create the Place Details (it is committed with the event) - place_detail = Place() - place_detail.set_name(PlaceName(value=title)) - place_detail.set_title(title) - # For RootsMagic etc. Place Details e.g. address, hospital, ... - place_detail.set_type((PlaceType.CUSTOM, _("Detail"))) - placeref = PlaceRef() - placeref.ref = place.get_handle() - place_detail.set_placeref_list([placeref]) - state.place = place_detail + title = self.__merge_address(addr, line) + state.addr = [] # list of place name components + state.addr_pf = [] # list of place name component types + + # We don't include Country here because bare country would not be much + # of an address, better to use raw one. + if not title: # indicates that no structured address provided + title = addr.get_street() + state.addr_pf = [PlaceType((PlaceType.CUSTOM, _("Address")))] + state.addr = [title] + sub_state.place.name.value = title else: - place = state.place - if place: - # We encounter an ADDR having previously encountered a PLAC - if len(place.get_alternate_locations()) != 0 and \ - not self.__get_first_loc(place).is_empty(): - # We have perviously found an ADDR, or have populated - # location from PLAC title - self.__add_msg(_("Location already populated; ADDR " - "ignored"), line, state) - # ignore this second ADDR, and use the old one - else: - # This is the first ADDR - place.add_alternate_locations(location) - else: - # The first thing we encounter is ADDR - state.place = Place() - place = state.place - place.add_alternate_locations(location) - place.set_name(PlaceName(value=title)) - place.set_title(title) - place.set_type((PlaceType.CUSTOM, _("Address"))) - - # merge notes etc into place - state.place.merge(sub_state.place) - - def __add_location(self, place, location): - """ - @param place: A place object we have found or created - @type place: Place - @param location: A location we want to add to this place - @type location: gen.lib.location - """ - for loc in place.get_alternate_locations(): - if loc.is_equivalent(location) == IDENTICAL: - return - place.add_alternate_locations(location) - - def __get_first_loc(self, place): - """ - @param place: A place object - @type place: Place - @return location: the first alternate location if any else None - @type location: gen.lib.location - """ - if len(place.get_alternate_locations()) == 0: - return None - else: - return place.get_alternate_locations()[0] + # structured address provided + for item, ptype in ((addr.get_street(), PlaceType.STREET), + (addr.get_city(), PlaceType.CITY), + (addr.get_state(), PlaceType.STATE), + (addr.get_country(), PlaceType.COUNTRY)): + item = item.replace("\n", " ").strip(',' + string.whitespace) + if not item: + continue # Don't store empties + state.addr_pf += [ptype] + state.addr += [item] + sub_state.place.name.value = state.addr[0] + sub_state.place.set_type(state.addr_pf[0]) + sub_state.place.set_title(title) + # store notes etc into place + sub_state.place.set_code(addr.postal) + sub_state.place.set_note_list(addr.note_list) + sub_state.place.set_citation_list(addr.citation_list) + sub_state.place.name.date = addr.date + state.addr_place = sub_state.place def __event_privacy(self, line, state): """ @@ -6195,33 +6182,25 @@ def __address_date(self, line, state): @param state: The current state @type state: CurrentState """ - state.addr.set_date_object(line.data) - - def __address_adr1(self, line, state): - """ - Parses the ADR1 line of an ADDR tag - - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - # The ADDR may already have been parsed by the level above - # assert state.addr.get_street() == "" - if state.addr.get_street() != "": - self.__add_msg(_("Warn: ADDR overwritten"), line, state) - state.addr.set_street(line.data) + if isinstance(state.addr, Address): + state.addr.set_date_object(line.data) + else: + # This causes dates below SUBMitter to be ignored + self.__not_recognized(line, state) - def __address_adr2(self, line, state): + def __address_adr(self, line, state): """ - Parses the ADR2 line of an ADDR tag + Parses the ADR1, ADR2, ADR3 line of an ADDR tag @param line: The current line in GedLine format @type line: GedLine @param state: The current state @type state: CurrentState """ - state.addr.set_locality(line.data) + if state.addr.street: + state.addr.street += ', ' + line.data.strip() + else: + state.addr.street = line.data.strip() def __address_city(self, line, state): """ @@ -6267,6 +6246,15 @@ def __address_country(self, line, state): """ state.addr.set_country(line.data) + def __address_phone(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.addr.set_phone(line.data) + def __address_sour(self, line, state): """ Parses the SOUR line of an ADDR tag @@ -6276,7 +6264,12 @@ def __address_sour(self, line, state): @param state: The current state @type state: CurrentState """ - state.addr.add_citation(self.handle_source(line, state.level, state)) + if isinstance(state.addr, Address): + state.addr.add_citation(self.handle_source(line, state.level, + state)) + else: + # This causes citations below SUBMitter to be ignored + self.__not_recognized(line, state) def __address_note(self, line, state): """ @@ -6287,7 +6280,11 @@ def __address_note(self, line, state): @param state: The current state @type state: CurrentState """ - self.__parse_note(line, state.addr, state) + if isinstance(state.addr, Address): + self.__parse_note(line, state.addr, state) + else: + # This causes notes below SUBMitter to be ignored + self.__not_recognized(line, state) def __citation_page(self, line, state): """ @@ -7025,15 +7022,13 @@ def __repo_addr(self, line, state): instead they put everything on a single line. Try to determine if this happened, and try to fix it. """ - free_form = line.data - sub_state = CurrentState(level=state.level + 1) sub_state.addr = Address() self.__parse_level(sub_state, self.parse_addr_tbl, self.__ignore) state.msg += sub_state.msg - self.__merge_address(free_form, sub_state.addr, line, state) + self.__merge_address(sub_state.addr, line) state.repo.add_address(sub_state.addr) def __repo_phon(self, line, state): @@ -7087,98 +7082,6 @@ def __repo_email(self, line, state): url.set_type(UrlType(UrlType.EMAIL)) state.repo.add_url(url) - def __location_adr1(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - if state.location.get_street() != "": - self.__add_msg(_("Warn: ADDR overwritten"), line, state) - state.location.set_street(line.data) - - def __location_adr2(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_locality(line.data) - - def __location_city(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_city(line.data) - - def __location_stae(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_state(line.data) - - def __location_post(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_postal_code(line.data) - - def __location_ctry(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_country(line.data) - - def __location_phone(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if not state.location: - state.location = Location() - state.location.set_phone(line.data) - - def __location_note(self, line, state): - """ - @param line: The current line in GedLine format - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - if state.event: - self.__parse_note(line, state.place, state) - else: - # This causes notes below SUBMitter to be ignored - self.__not_recognized(line, state) - def __optional_note(self, line, state): """ @param line: The current line in GedLine format @@ -7539,7 +7442,7 @@ def __header_plac(self, line, state): self.__parse_level(sub_state, self.place_form, self.__undefined) state.msg += sub_state.msg - def __place_form(self, line, state): + def __header_place_form(self, line, state): """ @param line: The current line in GedLine format @type line: GedLine @@ -7776,7 +7679,7 @@ def handle_source(self, line, level, state): # have got deleted by Chack and repair because the record is empty. # If we find the source record, the title is overwritten in # __source_title. - if not src.title: + if not src.get_title(): src.set_title(line.data) self.dbase.commit_source(src, self.trans) self.__parse_source_reference(citation, level, src.handle, state) @@ -7894,7 +7797,7 @@ def __build_event_pair(self, state, event_type, event_map, description): sub_state.event_ref = event_ref sub_state.event = event sub_state.person = state.person - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) if(description == 'Y' and event.date.is_empty() and @@ -7925,7 +7828,7 @@ def __build_family_event_pair(self, state, event_type, event_map, sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.pf = self.place_parser + sub_state.place_pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) state.msg += sub_state.msg @@ -8032,17 +7935,15 @@ def __subm_addr(self, line, state): @param state: The current state @type state: CurrentState """ - free_form = line.data - sub_state = CurrentState(level=state.level + 1) - sub_state.location = state.res + sub_state.addr = state.res - self.__parse_level(sub_state, self.parse_loc_tbl, self.__undefined) + self.__parse_level(sub_state, self.parse_addr_tbl, self.__undefined) state.msg += sub_state.msg - self.__merge_address(free_form, state.res, line, state) + self.__merge_address(state.res, line) # Researcher is a sub-type of LocationBase, so get_street and - # set_street which are used in routines called from self.parse_loc_tbl + # set_street which are used in routines called from self.parse_addr_tbl # work fine. # Unfortunately, Researcher also has get_address and set_address, so we # need to copy the street into that. @@ -8225,6 +8126,7 @@ def get_line_count(self): return self.lcnt + #------------------------------------------------------------------------- # # make_gedcom_date diff --git a/gramps/plugins/test/imports_test.py b/gramps/plugins/test/imports_test.py index 6e69edc8d33..e160ad882af 100644 --- a/gramps/plugins/test/imports_test.py +++ b/gramps/plugins/test/imports_test.py @@ -214,6 +214,7 @@ def tst(self, mockptime, mocktime, mockltime, mockdtime): fn2 = os.path.join(TEST_DIR, (file_name + ".gramps")) fres = os.path.join(TEMP_DIR, (file_name + ".difs")) fout = os.path.join(TEMP_DIR, (file_name + ".gramps")) + config.set('preferences.place-auto', True) if "_dfs" in tstfile: config.set('preferences.default-source', True) config.set('preferences.tag-on-import-format', "Imported") From 7e325a0faa1ea7fce39d42ea83bae55452db03bb Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 29 Sep 2017 15:49:04 -0500 Subject: [PATCH 04/26] GEPS045/043 Gedcom import support for Gedcom L place handling --- data/tests/imp_bug_8322_test.gramps | 122 +- data/tests/imp_place_test.ged | 335 +++++- data/tests/imp_place_test.gramps | 699 +++++++++-- data/tests/imp_sample.ged | 7 +- data/tests/imp_sample.gramps | 147 ++- gramps/plugins/importer/importgedcom.py | 5 +- gramps/plugins/lib/libgedcom.py | 1446 ++++++++++++++++++++--- 7 files changed, 2378 insertions(+), 383 deletions(-) diff --git a/data/tests/imp_bug_8322_test.gramps b/data/tests/imp_bug_8322_test.gramps index 8293bce2b4a..3f7a9e2cb7e 100644 --- a/data/tests/imp_bug_8322_test.gramps +++ b/data/tests/imp_bug_8322_test.gramps @@ -1,9 +1,9 @@ - - + +
- +
@@ -206,157 +206,185 @@ - + the place + - + the address - + + - + another address - + + - + the address + - + the place 2 + - + the address 2 - + + - + place test + - + address place test - + + - + address place test + - + different place test + - + address place test - + + - + address place test - + + - + a new place + - + address place test - + + - + address with no place + - + Woerden, Zuid-Holland, Netherlands + - + Kromwijkerkade 63 - + + - + Hasselt, Overijssel, Netherlands + - + Prinsenstraat 69 - + + - + Enschede, Overijssel, Netherlands + - + Calslaan 26-52 - + + - + Calslaan 26-44 - + + - + Calslaan 26-61 - + + - + Calslaan 26-61 + - + Amsterdam, Noord-Holland, Netherlands + - + Papendrechtstraat 37 - + + - + Olympiaplein 46-2 - + + - + remembered address that should be set into place - + + diff --git a/data/tests/imp_place_test.ged b/data/tests/imp_place_test.ged index cd4a77230ac..e8a23291501 100644 --- a/data/tests/imp_place_test.ged +++ b/data/tests/imp_place_test.ged @@ -7,27 +7,73 @@ 4 CONT Anyville, UT 12345 4 CONT USA 3 PHON 1-800-888-8888 +2 DATA Edited by hand by the Gramps Tester +3 DATE 26 AUG 2017 +3 COPR This is NOT copyrighted 2017 1 DEST GrampsTests -1 DATE 26 AUF 2017 +1 DATE 26 AUG 2017 1 FILE imp_place_test.ged 1 GEDC -2 VERS 5.5.1 2 FORM LINEAGE-LINKED +2 VERS 5.5.1 1 CHAR UTF-8 +2 VERS 3.0.0 1 SUBM @SUBM1@ +1 LANG MyLang 0 @SUBM1@ SUBM 1 NAME The Gramps Tester -1 ADDR 112 Main St., Open Source, Software, Is Great +1 ADDR TesterHouse, Annex, 112 Main St., Open Source, Software, Is Great 2 ADR1 112 Main St. 2 ADR2 Open Source 2 CITY Software 2 CTRY Is Great 2 POST 11111 +2 SOUR @S1@ +3 PAGE Invalid Address Citation for submitter +2 DATE DEC 1999 1 EMAIL gramps-users@@lists.sourceforge.net +1 EMAIL gramps-developers@@lists.sourceforge.net 1 FAX 9-999-999-9999 1 LANG MyLang 1 PHON 8-888-888-8888 1 WWW http://gramps-project.org +1 NOTE @N2@ +0 @I0000@ INDI +1 NAME Susan /Smith/ +2 GIVN Susan +2 SURN Smith +1 SEX F +1 FAMS @F0000@ +1 CHAN +2 DATE 17 SEP 2017 +3 TIME 15:17:04 +0 @I0001@ INDI +1 NAME Tom /Jones/ +2 GIVN Tom +2 SURN Jones +1 SEX M +1 FAMS @F0000@ +1 CHAN +2 DATE 17 SEP 2017 +3 TIME 15:17:04 +0 @I0002@ INDI +1 NAME Tim /Jones/ +2 GIVN Tim +2 SURN Jones +1 SEX M +1 FAMC @F0000@ +2 PEDI birth +1 CHAN +2 DATE 17 SEP 2017 +3 TIME 15:17:04 +0 @F0000@ FAM +1 HUSB @I0001@ +1 WIFE @I0000@ +1 CHIL @I0002@ +1 ADDR the Jones address +1 CHAN +2 DATE 17 SEP 2017 +3 TIME 15:17:04 0 @I310@ INDI 1 NAME Living 1 SEX M @@ -75,6 +121,10 @@ 2 DATE 1965 2 ADDR raw address 1 RESI +2 DATE FEB 1965 +2 ADDR raw address +3 NOTE Check merge of raw addresses +1 RESI 2 DATE 1966 2 ADDR Raw address in Mytown 3 CITY Mytown @@ -88,6 +138,284 @@ 2 ADDR the address 2 ADDR second address ignored 3 NOTE second address ignored +1 RESI +2 DATE 1968 +2 PLAC Myhouse, 123 Main, Mycity, Mycounty, Mystate, USA +3 FORM building, street, city, county, state, country +3 FONE Mah Haas +4 TYPE Texan +3 ROMN Mah Haas, durnit +4 TYPE Texan +3 MAP +4 LATI N31.9151389 +4 LONG W98.5375957 +3 NOTE Attached to Myhouse PLAC +3 _POST 23456 +4 DATE JAN 2000 +3 _LOC @P1@ +1 RESI +2 DATE 1969 +2 ADDR Myhouse, 123 Main +2 PLAC My city, Mycounty, Mystate, USA, 23456 +3 FORM city, county, state, country, Zip Code +3 _LOC @P3@ +2 NOTE Find by _LOC (name is different) +1 RESI +2 DATE 1970 +2 PLAC Mytown, Mystate, USA +3 FORM city, state, country +3 _GOV MYPLACE002 +3 _FPOST 23456NA +3 _MAIDENHEAD No idea what this is... +3 NOTE Merge with Line 79 +1 RESI +2 DATE 1971 +2 ADDR Myhouse, 123 Main, Mycity, Mycounty, Mystate, USA, 23456 +3 ADR1 123 Main +3 POST 23456 +3 CITY Mycity +3 STAE Mystate +3 CTRY USA +1 RESI +2 DATE 1972 +2 PLAC Mycity, Mycounty, Mystate, USA +3 FORM city, county, state, country +3 _POST 34567 +1 RESI +2 DATE 1973 +2 PLAC USA +3 FORM country +3 NOTE tests for match with no placeref and only country +1 RESI +2 DATE 1974 +2 PLAC Texas, United States +3 _GOV object_276695 +3 NOTE setup for next test +1 RESI +2 DATE 1975 +2 PLAC Texas, United States +3 FORM state, country +3 _GOV object_276695 +3 NOTE tests for match with _GOV and different placetype from default +1 RESI +2 DATE 1976 +2 PLAC Texas, United States +3 FORM state_again, country +3 _GOV object_276695 +3 NOTE tests for match with _GOV and different placetype from already set +1 RESI +2 DATE 1977 +2 ADDR Myhouse, 123 Main, Mycity, , Mystate, USA, 23456 +3 ADR1 123 Main +3 POST 23456 +3 CITY Mycity +3 STAE Mystate +3 CTRY USA +2 NOTE tests for empty free-form item. +1 RESI +2 DATE 1978 +2 PLAC My house +3 _LOC @P3@ +3 NOTE Test match by LOC +1 RESI +2 DATE 1979 +2 PLAC Mycity, Mycounty, Mystate, USA, 23456, North America +3 FORM city, county, state, country, Zip Code, Continent +3 NOTE Tests extra beyond country items +1 RESI +2 DATE 1980 +2 ADDR the address unknown +2 PLAC Texas, USA +3 FORM state, country +3 NOTE Tests match except for xref +1 RESI +2 DATE 1981 +2 PLAC Usa +3 _LOC @P11@ +1 RESI +2 DATE 1982 +2 PLAC Ouchio, USX, North America +3 FORM State, Country, Continent +3 _LOC @MYPLACE101@ +1 RESI +2 DATE JUN 1960 +2 PLAC the place unknown +2 ADDR the address unknown +3 NOTE address enclosed by place, both new +0 @P1@ _LOC +1 NAME Myhouse +2 DATE JAN 2000 +2 _NAMC Where I lived +2 ABBR Myhouse +3 TYPE What does this mean? +2 LANG English +2 SOUR @S1@ +3 PAGE Myhouse Name Citation +1 TYPE building +2 DATE JAN 2000 +2 SOUR @S1@ +3 PAGE Myhouse Type Citation +1 EVEN Move-in to Myhouse +2 TYPE Occupancy +2 PLAC Ohio +3 _LOC @P8@ +2 DATE 11 JAN 2000 +1 _LOC @P2@ +2 TYPE POLI +2 DATE JAN 2000 +2 SOUR @S1@ +3 PAGE Myhouse located in Mycity Citation +1 _LOC @P13@ +2 TYPE RELI +2 DATE AFTER JAN 2012 +1 _DMGD Another Field I'm unfamiliar with +2 TYPE WhoKnows +2 DATE JAN 2000 +2 SOUR @S1@ +3 PAGE Myhouse DMGD Citation +1 _AIDN Another Field I'm unfamiliar with +2 TYPE Whoknows +2 DATE JAN 2000 +2 SOUR @S1@ +3 PAGE Myhouse AIDN Citation +1 OBJE @M1@ +1 NOTE @N1@ +1 SOUR @S1@ +2 PAGE Myhouse Citation +1 CHAN +2 DATE 15 SEP 2017 +3 TIME 09:09:09 +0 @P2@ _LOC +1 NAME 123 Main +1 TYPE Street +1 _LOC @P3@ +0 @P3@ _LOC +1 NAME Mycity +1 TYPE City +1 _GOV MYPLACE001 +1 MAP +2 LATI N31.92 +2 LONG W98.54 +1 _POST 23456 +2 DATE JAN 2000 +2 SOUR @S1@ +3 PAGE Mycity POST Citation +1 _LOC @P4@ +0 @P4@ _LOC +1 NAME Mycounty +2 LANG English +1 NAME Mycounty +2 LANG English +1 NAME My cool county +2 LANG Hippy +1 TYPE County +1 _LOC @P5@ +0 @P5@ _LOC +1 NAME Mystate +1 TYPE state +1 _LOC @P6@ +0 @P6@ _LOC +1 NAME USA +2 ABBR United States of America +1 TYPE country +0 @P7@ _LOC +1 NAME Mytown +1 TYPE town +1 _GOV MYPLACE002 +1 NOTE Find by _GOV, should be merged with lines 79, 114 +1 _FPOST 23456NA +1 _FSTAE Mstate Not Applicable +1 _FCTRY USA Not Applicable +1 _MAIDENHEAD No idea what this is... +1 _MAIDENHEAD No idea what this is... +1 _LOC @P5@ +0 @P8@ _LOC +1 NAME Ohio +1 TYPE state +1 _LOC @P9@ +1 _LOC @P11@ +1 _LOC @P11@ +1 _LOC @P12@ +2 TYPE POLI +0 @P9@ _LOC +1 NAME United States +1 TYPE country +1 _GOV object_276534 +1 _LOC @P10@ +0 @P11@ _LOC +1 NAME United States of America +1 TYPE Country +2 _GOVTYPE 50 +1 _GOV object_276534a +0 @P12@ _LOC +1 NAME Testers Country +1 TYPE country +1 _GOV Tester001 +0 @P13@ _LOC +1 NAME Testers Parish +1 TYPE Parish +2 _GOVTYPE 29 +0 @MYPLACE101@ _LOC +1 NAME Ouchio +1 TYPE State +1 _POST 34567 +2 DATE JAN 2000 +1 _GOV MYPLACE101 +1 MAP +2 LATI N31.92 +2 LONG W98.54 +1 _LOC @P0101@ +2 TYPE POLI +1 SOUR @S1@ +2 PAGE Citation for Ouchio +1 CHAN +2 DATE 2 MAR 2020 +3 TIME 15:40:05 +0 @P0100@ _LOC +1 NAME Tehas +1 TYPE State +2 DATE AFT 1900 +1 TYPE Country +2 DATE BEF 1900 +1 _LOC @P0101@ +2 DATE AFT 1900 +2 TYPE POLI +1 _LOC @P0102@ +2 DATE BEF 1900 +2 TYPE POLI +1 CHAN +2 DATE 2 MAR 2020 +3 TIME 15:38:45 +0 @P0101@ _LOC +1 NAME USX +2 ABBR United States of Tester +3 TYPE Abbreviation +1 TYPE Country +2 DATE AFT 1900 +1 TYPE State +2 DATE BEF 1900 +1 _LOC @P0100@ +2 DATE BEF 1900 +2 TYPE POLI +1 _LOC @P0102@ +2 DATE AFT 1900 +2 TYPE POLI +1 CHAN +2 DATE 2 MAR 2020 +3 TIME 15:38:09 +0 @P0102@ _LOC +1 NAME North America +1 TYPE Continent +1 CHAN +2 DATE 2 MAR 2020 +3 TIME 15:37:17 +0 @M1@ OBJE +1 FILE myhouse.jpg +2 FORM jpg +3 TYPE photo +0 @N1@ NOTE Myhouse was my first purchase! +0 @N2@ NOTE We use this person's RESI for place testing. +0 @N3@ NOTE 0 @S1@ SOUR 1 AUTH The Gramps Test guys 1 PUBL 2017 @@ -97,4 +425,5 @@ 1 NAME Gramps GitHub Library + 0 TRLR diff --git a/data/tests/imp_place_test.gramps b/data/tests/imp_place_test.gramps index 5b18966ae8f..eafa2a17d6f 100644 --- a/data/tests/imp_place_test.gramps +++ b/data/tests/imp_place_test.gramps @@ -1,12 +1,12 @@ - - + +
- + The Gramps Tester - 112 Main St., Open Source + TesterHouse, Annex, 112 Main St., Open Source Software Is Great 11111 @@ -15,211 +15,736 @@
- + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + - + Residence - + + + + Residence + + - + Residence - + - + Residence - + + + + Residence + + + + + Residence + + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Residence + + + + + Occupancy + + + Move-in to Myhouse - + + F + + Susan + Smith + + + + + M + + Tom + Jones + + + + + M + + Tim + Jones + + + + M Living - - + - + - - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - + Citation for 'the address unknown' 2 - + - + Citation for 'the place unknown' 2 - + + + + Myhouse Name Citation + 2 + + + + Myhouse Type Citation + 2 + + + + Myhouse located in Mycity Citation + 2 + + + + Myhouse DMGD Citation + 2 + + + + Myhouse AIDN Citation + 2 + + + + Myhouse Citation + 2 + + + + Mycity POST Citation + 2 + + + + Citation for Ouchio + 2 + - + The Gramps Test Library The Gramps Test guys 2017 - + - + the place unknown - - + + + - + the address unknown - - - - + + + + + - + the place + - + 1962, the place - - + + + - + USA - + + + + + + - + Mystate, USA - + + - + Mytown, Mystate, USA - + + + + + + + - + 1963 address, Mylocality, Mytown, Mystate, USA - - - + + + + - + Mylocality, Mytown, Mystate, USA - + + - + Full 1964 place, Mylocality, Mytown, Mystate, USA - 12345 - + + + - + Addr detail on Full 1964 place - - + + + - + raw address + + - + Raw address in Mytown, Mytown, Mystate, USA - - + + + - + the address - + + + + + Myhouse, 123 Main, Mycity, Mycounty, Mystate, USA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Myhouse, 123 Main + + + + + + Mycity, Mystate, USA + + + + + + Myhouse, 123 Main, Mycounty, Mycity, Mystate, USA + + + + + + + Mycounty, Mystate, USA + + + + + + + + Mycity, Mycounty, Mystate, USA + + + + + + + + + + + + Texas, United States + + + + + + + + + + United States + + + + + Myhouse, 123 Main, Mycity, Mystate, USA + + + + + + + North America + + + + + USA, North America + + + + + + Mystate, USA, North America + + + + + + Mycounty, Mystate, USA, North America + + + + + + Mycity, Mycounty, Mystate, USA, North America + + + + + + + + Texas, USA + + + + + + + the address unknown + + + + + + United States of America + + + + + Ouchio, USX, Tehas + + + + + + + + + Ohio, United States + + + + + + + + 123 Main, Mycity, Mycounty, Mystate, USA + + + + + + United States + + + + + Testers Country + + + + + Testers Parish + + + + + Tehas, USX + + + + + + + + + + + + + + + + USX, Tehas + + + + + + + + + + + + + + + + + + Unknown + + + + + + + + + + - + Gramps GitHub Library Library - + + We use this person's RESI for place testing. + + Records not imported into SUBM (Submitter): (@SUBM1@) The Gramps Tester: -Line ignored as not understood Line 28: 1 LANG MyLang +Line ignored as not understood Line 31: 2 SOUR @S1@ +Skipped subordinate line Line 32: 3 PAGE Invalid Address Citation for submitter +Line ignored as not understood Line 33: 2 DATE 1999-12-00 +Line ignored as not understood Line 37: 1 LANG MyLang - + + Records not imported into FAM (family) Gramps ID F0000: + +Tag recognized but not supported Line 73: 1 ADDR the Jones address + + + + address enclosed by place, both new - + address enclosed by place, both existing note also merged into existing place - + check that this note is retained when the place is merged into existing place - + setup the place - + the ADDR is new, but PLAC already exists, we skip enclosure for ADDR portion - + the Mylocality place already exists, other stuff new - + + Check merge of raw addresses + + New raw address, enclosed by Mytown - + + Place Phonetic Variation: Mah Haas + Type: Texan  +Place Romanized Variation: Mah Haas, durnit + Type: Texan  +Name: Myhouse + Place Name Addition: Where I lived  + + + + Attached to Myhouse PLAC + + + Find by _LOC (name is different) + + + Merge with Line 79 + + + tests for match with no placeref and only country + + + setup for next test + + + tests for match with _GOV and different placetype from default + + + tests for match with _GOV and different placetype from already set + + + tests for empty free-form item. + + + Test match by LOC + + + Tests extra beyond country items + + + Tests match except for xref + + Records not imported into INDI (individual) Gramps ID I0310: -A second PLAC ignored Line 87: 2 PLAC second place ignored -Line ignored as not understood Line 89: 2 ADDR second address ignored -Skipped subordinate line Line 90: 3 NOTE second address ignored +A second PLAC ignored Line 137: 2 PLAC second place ignored +Line ignored as not understood Line 139: 2 ADDR second address ignored +Skipped subordinate line Line 140: 3 NOTE second address ignored +Tag recognized but not supported Line 168: 3 _FPOST 23456NA + + + + + Myhouse was my first purchase! + + + Find by _GOV, should be merged with lines 79, 114 + + + Records not imported into Place Gramps ID P0007: + +Tag recognized but not supported Line 326: 1 _FPOST 23456NA +Tag recognized but not supported Line 327: 1 _FSTAE Mstate Not Applicable +Tag recognized but not supported Line 328: 1 _FCTRY USA Not Applicable + + + + + Records not imported into OBJE (multi-media object) Gramps ID M1: + +Could not import myhouse.jpg Line 413: 1 FILE myhouse.jpg + + Objects referenced by this note were missing in a file imported on 12/25/1999 12:00:00 AM. +
diff --git a/data/tests/imp_sample.ged b/data/tests/imp_sample.ged index deb76581f4a..95cb5e27add 100644 --- a/data/tests/imp_sample.ged +++ b/data/tests/imp_sample.ged @@ -201,9 +201,10 @@ 2 PLAC San Francisco, San Francisco Co., CA 1 RESI 2 DATE 1980 -2 ADDR 456 Main St., The Village, San Francisco, CA, USA -3 ADR1 456 Main St. -3 ADR2 The Village +2 ADDR 459 Main St., The Village, San Francisco, CA, USA +3 ADR1 459 Main St. +3 ADR2 The Village, San Francisco, CA, 87654 +3 ADR3 USA 3 CITY San Francisco 3 CTRY USA 3 PHON 555-666-7777 diff --git a/data/tests/imp_sample.gramps b/data/tests/imp_sample.gramps index 4d5ae366455..f2a5f8086be 100644 --- a/data/tests/imp_sample.gramps +++ b/data/tests/imp_sample.gramps @@ -1,7 +1,7 @@ - - + +
@@ -163,6 +163,7 @@ Birth + No Date Information Death @@ -854,7 +855,7 @@ - + @@ -1327,134 +1328,166 @@ - + Rønne, Bornholm, Denmark + + - + Löderup, Malmöhus Län, Sweden + - + Sparks, Washoe Co., NV + - + San Francisco, San Francisco Co., CA + - + Gladsax, Kristianstad Län, Sweden + - + Reno, Washoe Co., NV + - + USA + - + CA, USA - + + - + San Francisco, CA, USA - + + - - 456 Main St., The Village, San Francisco, CA, USA - - + + 459 Main St., The Village, 87654, San Francisco, CA, USA + + + - + Hayward, Alameda Co., CA + - + Community Presbyterian Church, Danville, CA + - + Sweden + - + Grostorp, Kristianstad Län, Sweden + - + Copenhagen, Denmark + - + Sweden + - + Hoya/Jona/Hoia, Sweden + - + Simrishamn, Kristianstad Län, Sweden + - + Fremont, Alameda Co., CA + - + Denver, Denver Co., CO + - + Sacramento, Sacramento Co., CA + - + Santa Rosa, Sonoma Co., CA + - + San Jose, Santa Clara Co., CA + - + UC Berkeley + - + Smestorp, Kristianstad Län, Sweden + - + Tommarp, Kristianstad Län, Sweden + - + Rønne Bornholm, Denmark + - + Oronoko, Berrien, Michigan, USA + - + Shemps + - + Woodland, Yolo Co., CA + - + San Ramon, Conta Costa Co., CA + @@ -1466,13 +1499,13 @@
- + - + - + @@ -1576,7 +1609,7 @@ Filename omitted Line 48: Records not imported into INDI (individual) Gramps ID I0018: -Tag recognized but not supported Line 244: 2 TYPE first generaton +Tag recognized but not supported Line 247: 2 TYPE first generaton - + Objects referenced by this note were missing in a file imported on 12/25/1999 12:00:00 AM. diff --git a/gramps/plugins/importer/importgedcom.py b/gramps/plugins/importer/importgedcom.py index ac2c604447d..659291a1676 100644 --- a/gramps/plugins/importer/importgedcom.py +++ b/gramps/plugins/importer/importgedcom.py @@ -48,6 +48,7 @@ module = __import__("gramps.plugins.lib.libgedcom", fromlist=["gramps.plugins.lib"]) # why o why ?? as above! import imp +import time imp.reload(module) from gramps.gen.config import config @@ -108,9 +109,10 @@ def importData(database, filename, user): assert(isinstance(code_set, str)) + t1 = time.time() try: ifile = open(filename, "rb") - stage_one = libgedcom.GedcomStageOne(ifile) + stage_one = libgedcom.GedcomStageOne(ifile, database) stage_one.parse() if code_set: @@ -151,4 +153,5 @@ def importData(database, filename, user): return ## a "GEDCOM import report" happens in GedcomParser so this is not needed: ## (but the imports_test.py unittest currently requires it, so here it is) + print("Import time", time.time() - t1) return ImportInfo({_("Results"): _("done")}) diff --git a/gramps/plugins/lib/libgedcom.py b/gramps/plugins/lib/libgedcom.py index 5fbe092f277..693c77ad994 100644 --- a/gramps/plugins/lib/libgedcom.py +++ b/gramps/plugins/lib/libgedcom.py @@ -88,13 +88,14 @@ # standard python modules # #------------------------------------------------------------------------- -import os +import os, sys import re import time # from xml.parsers.expat import ParserCreate from collections import defaultdict, OrderedDict import string -import mimetypes +from mimetypes import guess_type as mimetypes_guess_type +from mimetypes import types_map as mimetypes_types_map from io import StringIO, TextIOWrapper from urllib.parse import urlparse @@ -118,10 +119,12 @@ Address, Attribute, AttributeType, ChildRef, ChildRefType, Citation, Date, Event, EventRef, EventRoleType, EventType, Family, FamilyRelType, LdsOrd, Location, Media, - MediaRef, Name, NameType, Note, NoteType, Person, PersonRef, Place, + MediaRef, Name, NameType, Note, NoteType, Person, PersonRef, + Place, PlaceAbbrev, PlaceAbbrevType, PlaceName, PlaceHierType, + PlaceRef, PlaceType, PlaceGroupType as P_G, RepoRef, Repository, RepositoryType, Researcher, Source, SourceMediaType, SrcAttribute, - Surname, Tag, Url, UrlType, PlaceType, PlaceRef, PlaceName) + Surname, Tag, Url, UrlType) from gramps.gen.db import DbTxn from gramps.gen.updatecallback import UpdateCallback from gramps.gen.utils.file import media_path @@ -131,8 +134,10 @@ from gramps.gen.datehandler._dateparser import DateParser from gramps.gen.db.dbconst import EVENT_KEY from gramps.gen.lib import (StyledText, StyledTextTag, StyledTextTagType) +from gramps.gen.lib.const import DIFFERENT from gramps.gen.lib.urlbase import UrlBase -from gramps.gen.utils.grampslocale import GrampsLocale +from gramps.gen.datehandler import displayer +from gramps.gen.utils.grampslocale import GrampsLocale, _LOCALE_NAMES #------------------------------------------------------------------------- # @@ -273,9 +278,22 @@ TOKEN__TEXT = 136 TOKEN__DATE = 137 TOKEN_ADR3 = 138 +TOKEN__GOV = 139 +TOKEN__POST = 140 +TOKEN__FPOST = 141 +TOKEN__MAIDENHEAD = 142 +TOKEN__FSTAE = 143 +TOKEN__FCTRY = 144 +TOKEN__NAMC = 145 +TOKEN__DMGD = 146 +TOKEN__AIDN = 147 +TOKEN_FONE = 148 +TOKEN_ROMN = 149 +TOKEN__GOVTYPE = 150 TOKENS = { "_ADPN" : TOKEN__ADPN, + "_AIDN" : TOKEN__AIDN, # place ADMINISTRATIVE_IDENTIFIER "_AKA" : TOKEN__AKA, "_AKAN" : TOKEN__AKA, "_ALIA" : TOKEN_ALIA, @@ -286,17 +304,24 @@ "_DATE" : TOKEN__DATE, "_DATE2" : TOKEN_IGNORE, "_DETAIL" : TOKEN_IGNORE, + "_DMGD" : TOKEN__DMGD, # place DEMOGRAPHICAL_DATA "_EMAIL" : TOKEN_EMAIL, "_E-MAIL" : TOKEN_EMAIL, + "_FCTRY" : TOKEN__FCTRY, # FOKO_STATE_IDENTIFIER + "_FPOST" : TOKEN__FPOST, # FOKO_POSTCODE "_FREL" : TOKEN__FREL, + "_FSTAE" : TOKEN__FSTAE, # FOKO_TERRITORY_IDENTIFIER "_FSFTID" : TOKEN__FSFTID, "_GODP" : TOKEN__GODP, + "_GOV" : TOKEN__GOV, # GOV_IDENTIFIER + "_GOVTYPE" : TOKEN__GOVTYPE, # GOV place type ID "_ITALIC" : TOKEN_IGNORE, "_JUST" : TOKEN__JUST, # FTM Citation Quality Justification "_LEVEL" : TOKEN_IGNORE, "_LINK" : TOKEN__LINK, "_LKD" : TOKEN__LKD, - "_LOC" : TOKEN__LOC, + "_LOC" : TOKEN__LOC, # location data record tag + "_MAIDENHEAD" : TOKEN__MAIDENHEAD, # place MAIDENHEAD_LOCATOR "_MAR" : TOKEN__MAR, "_MARN" : TOKEN__MARN, "_MARNM" : TOKEN__MARNM, @@ -304,9 +329,11 @@ "_MEDI" : TOKEN_MEDI, "_MREL" : TOKEN__MREL, "_NAME" : TOKEN__NAME, + "_NAMC" : TOKEN__NAMC, # PLACE_NAME_ADDITION "_PAREN" : TOKEN_IGNORE, "_PHOTO" : TOKEN__PHOTO, "_PLACE" : TOKEN_IGNORE, + "_POST" : TOKEN__POST, # POSTAL_CODE "_PREF" : TOKEN__PRIMARY, "_PRIM" : TOKEN__PRIM, "_PRIMARY" : TOKEN__PRIMARY, @@ -398,6 +425,7 @@ "FAMS" : TOKEN_FAMS, "FAX" : TOKEN_FAX, "FILE" : TOKEN_FILE, + "FONE" : TOKEN_FONE, # Phonetic variation "FORM" : TOKEN_FORM, "GEDC" : TOKEN_GEDC, "GEDCOM" : TOKEN_GEDC, @@ -455,6 +483,7 @@ "RFN" : TOKEN_RFN, "RIN" : TOKEN_RIN, "ROLE" : TOKEN_ROLE, + "ROMN" : TOKEN_ROMN, # ROMANIZED_VARIATION "SCHEMA" : TOKEN__SCHEMA, "SEX" : TOKEN_SEX, "SLGC" : TOKEN_SLGC, @@ -1563,7 +1592,8 @@ class CurrentState: """ Keep track of the current state variables. """ - def __init__(self, person=None, level=0, event=None, event_ref=None): + def __init__(self, person=None, level=0, event=None, event_ref=None, + place_pf=None): """ Initialize the object. """ @@ -1582,8 +1612,9 @@ def __init__(self, person=None, level=0, event=None, event_ref=None): self.filename = "" self.title = "" self.place = None - self.place_pf = None # PLAC.FORM parser + self.place_pf = place_pf if place_pf else [] # PLAC.FORM data self.place_fields = None # method for parsing places + self.place_gov = None # Hold the _GOV ID self.addr = None # Hold ADDR structure self.addr_place = None # The Place that will hold the ADDR data self.addr_pf = None # the FORM for the ADDR @@ -1618,72 +1649,6 @@ def __setattr__(self, name, value): self.__dict__[name] = value -#------------------------------------------------------------------------- -# -# PlaceParser -# -#------------------------------------------------------------------------- -class PlaceParser: - """ - Provide the ability to parse GEDCOM FORM statements for places, and - mapping the text components to PlaceType values based on the FORM - statement. - - Note that postal codes are a special case. - - For now this assumes FORM statements are in English or the local language; - a questionable assumption. If not recognized, they will be treated as - CUSTOM grampstypes. - """ - POSTAL = -99 - - __field_map = { - 'Addr' : PlaceType.STREET, - 'Subdivision' : PlaceType.STREET, - 'Addr1' : PlaceType.STREET, - 'Adr1' : PlaceType.STREET, - 'Addr2' : PlaceType.LOCALITY, - 'Adr2' : PlaceType.LOCALITY, - 'State/province': PlaceType.STATE, - 'Area code' : POSTAL, - 'Post code' : POSTAL, - 'Postal' : POSTAL, - 'Zipcode' : POSTAL, - 'Zip code' : POSTAL, } - - def __init__(self, line=None): - self.pf_list = [] - - if line: - self.parse_form(line) - - def parse_form(self, line): - """ - Parses the GEDCOM PLAC.FORM into a list of PlaceTypes(if possible). - It does this my mapping the text strings (separated by commas) to the - corresponding PlaceType by trying our own __field_map, then the - translated PlaceType set, then the English PlaceType set. - """ - self.pf_list = [] - for item in line.data.split(','): - item = item.strip().capitalize() - type_num = self.__field_map.get(item, None) - if type_num == self.POSTAL: - pass - elif type_num is not None: - type_num = PlaceType(type_num) - else: - type_num = PlaceType(item) - if type_num.is_custom(): - type_num = PlaceType() - type_num.set_from_xml_str(item) - self.pf_list.append(type_num) - - def get_pf(self): - """ return the list of PlaceTypes associated with the PLAC.FORM """ - return self.pf_list - - #------------------------------------------------------------------------- # # IdFinder @@ -1841,15 +1806,15 @@ def __parse_name_personal(text): return name @staticmethod - def __add_placeref(place, ref_hndl, date=None): + def __add_placeref(place, ref_hndl): """ Adds a PlaceRef to a Place. It checks for duplicates before adding. """ pref = PlaceRef() pref.ref = ref_hndl - pref.date = date + pref.set_type(PlaceHierType.ADMIN) for ref in place.placeref_list: - if ref.serialize() == pref.serialize(): + if ref.is_equivalent(pref) != DIFFERENT: return place.add_placeref(pref) @@ -1871,8 +1836,11 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.emapper = IdFinder(event_ids, dbase.event_prefix) self.famc_map = stage_one.get_famc_map() self.fams_map = stage_one.get_fams_map() + self.place_type_dict = stage_one.get_place_type_dict() + self.loc_loc_mode = stage_one.loc_loc_mode + self.loc_gov = stage_one.loc_gov + self.place_pf = None - self.place_parser = PlaceParser() self.inline_srcs = OrderedDict() self.media_map = {} self.note_type_map = {} @@ -1925,6 +1893,10 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.dbase.has_note_gramps_id, self.dbase.find_next_note_gramps_id, self.dbase.nid2user_format) + # The place IdMapper was moved to GedcomStageOne so that gids could be + # created from _LOC xref early on, and not be affected by normal place + # creation. + self.lid_map = stage_one.get_lid_map() # Despite the name, the following are Gramps ID to handle self.gid2id = {} # Person @@ -1933,6 +1905,9 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.oid2id = {} # Media self.rid2id = {} # Repo self.nid2id = {} # Note + self.lid2id = {} # Place + self.locs_list = [] + # # Parse table for <> below the level 0 SUBM tag # @@ -2088,6 +2063,8 @@ def __init__(self, dbase, ifile, filename, user, stage_one, TOKEN__AKA : self.__name_aka, # PAF and AncestQuest TOKEN_TYPE : self.__name_type, # This is legal GEDCOM 5.5.1 TOKEN_BIRT : self.__ignore, + TOKEN_FONE : self.__ignore, # TODO legal GEDCOM 5.5.1 + TOKEN_ROMN : self.__ignore, # TODO legal GEDCOM 5.5.1 TOKEN_DATE : self.__name_date, # This handles date as a subsidiary of "1 ALIA" which might be used # by Family Tree Maker and Reunion, and by cheating (handling a @@ -2499,16 +2476,33 @@ def __init__(self, dbase, ifile, filename, user, stage_one, TOKEN_FORM : self.__place_form, # +1 FONE {0:M} # +2 TYPE {1:1} + TOKEN_FONE : self.__place_fone, # +1 ROMN {0:M} # +2 TYPE {1:1} + TOKEN_ROMN : self.__place_romn, # +1 MAP {0:1} TOKEN_MAP : self.__place_map, # self.place_map_tbl # Not legal. TOKEN_OBJE : self.__place_object, TOKEN_SOUR : self.__place_sour, - TOKEN__LOC : self.__ignore, # Not legal, but generated by Ultimate Family Tree TOKEN_QUAY : self.__ignore, + # Extensions + # +1 _POST {0:M} + # +2 DATE {0:1} + TOKEN__POST : self.__place_post, + # +1 _FPOST {0:M} + TOKEN__FPOST : self.__ignore, + # +1 _MAIDENHEAD {0:1} + TOKEN__MAIDENHEAD : self.__place_maiden, + # +1 _GOV {0:1} + TOKEN__GOV : self.__place_gov, + # +1 _FSTAE {0:1} + TOKEN__FSTAE : self.__ignore, + # +1 _FCTRY {0:1} + TOKEN__FCTRY : self.__ignore, + # +1 _LOC @@ {0:1} + TOKEN__LOC : self.__place_loc, } self.func_list.append(self.event_place_map) @@ -2520,6 +2514,89 @@ def __init__(self, dbase, ifile, filename, user, stage_one, } self.func_list.append(self.place_map_tbl) + # 0 @@ _LOC + self._loc_tbl = { + # 1 NAME {1:M} + TOKEN_NAME : self.__place_name, # self.loc_name_tbl + # 1 TYPE {0:M} + # 2 DATE {0:1} + # 2 << SOURCE_CITATION >> {0:M} + TOKEN_TYPE : self.__place_type, + # 1 _FPOST {0:M} + # 2 DATE {0:1} + TOKEN__FPOST : self.__ignore, + # 1 _POST {0:M} + # 2 DATE {0:1} + # 2 << SOURCE_CITATION >> {0:M} + TOKEN__POST : self.__place_post, + # 1 _GOV {0:1} + TOKEN__GOV : self.__place_gov, + # 1 _FSTAE {0:1} + TOKEN__FSTAE : self.__ignore, + # 1 _FCTRY {0:1} + TOKEN__FCTRY : self.__ignore, + # 1 MAP {0:1} + TOKEN_MAP : self.__place_map, + # 1 _MAIDENHEAD {0:1} + TOKEN__MAIDENHEAD : self.__place_maiden, + # 1 EVEN [ | ] {0:M} + # 2 << EVENT_DETAIL >> {0:1} + TOKEN_EVEN : self.__place_even, + # 1 _LOC @ @ {0:M} + # 2 TYPE {1:1} + # 2 DATE {0:1} + # 2 << SOURCE_CITATION >> {0:M} + TOKEN__LOC : self.__place_loc_ref, + # 1 _DMGD {0:M} + # 2 DATE {0:1} + # 2 << SOURCE_CITATION >> {0:M} + # 2 TYPE {1:1} + TOKEN__DMGD : self.__place_demo, + # 1 _AIDN {0:M} + # 2 DATE {0:1} + # 2 << SOURCE_CITATION >> {0:M} + # 2 TYPE {1:1} + TOKEN__AIDN : self.__place_aidn, + # 1 << MULTIMEDIA_LINK >> {0:M} + TOKEN_OBJE : self.__place_object, + # 1 << NOTE_STRUCTURE >> {0:M} + TOKEN_NOTE : self.__place_note, + TOKEN_RNOTE : self.__place_note, + # 1 << SOURCE_CITATION >> {0:M} + TOKEN_SOUR : self.__place_sour, + # 1 << CHANGE_DATE >> {0:1} + TOKEN_CHAN : self.__place_chan, + } + self.func_list.append(self._loc_tbl) + + # 1 NAME {1:M} + self.loc_name_tbl = { + # 2 DATE {0:1} + TOKEN_DATE : self.__place_name_date, + # 2 _NAMC {0:1} + TOKEN__NAMC : self.__place_name_namc, + # 2 ABBR {0:M} + TOKEN_ABBR : self.__place_name_abbr, + # 3 TYPE {0:1} + # 2 LANG {0:1} + TOKEN_LANG : self.__place_name_lang, + # 2 << SOURCE_CITATION >> {0:M} stored directly in place.citation + TOKEN_SOUR : self.__place_name_sour, + } + self.func_list.append(self.loc_name_tbl) + + self.date_cit_type_tbl = { + # 2 DATE {0:1} + TOKEN_DATE : self.__event_date, + # 2 << SOURCE_CITATION >> {0:M} + TOKEN_SOUR : self.__event_source, + # 2 TYPE {1:1} + TOKEN_TYPE : self.__place_subtype, + # 2 _GOVTYPE {0:1} + TOKEN__GOVTYPE : self.__place_subtype, + } + self.func_list.append(self.date_cit_type_tbl) + self.repo_ref_tbl = { TOKEN_CALN : self.__repo_ref_call, TOKEN_NOTE : self.__repo_ref_note, @@ -2662,6 +2739,8 @@ def __init__(self, dbase, ifile, filename, user, stage_one, self.func_list.append(self.note_parse_tbl) # look for existing place titles, build a map + # TODO Newer places don't have titles (Auto title gen); we need to + # switch to PlaceNames here and throughout. self.place_names = defaultdict(list) cursor = dbase.get_place_cursor() data = next(cursor) @@ -2728,10 +2807,12 @@ def parse_gedcom_file(self, use_trans=False): src.set_handle(handle) src.set_title(title) self.dbase.add_source(src, self.trans) - self.__clean_up() - + self.set_total(len(self.locs_list)) + self.__loc_postprocess() if not self.dbase.get_feature("skip-check-xref"): self.__check_xref() + self.__clean_up() + self.dbase.enable_signals() self.dbase.request_rebuild() if self.number_of_errors == 0: @@ -2896,42 +2977,128 @@ def __find_or_create_note(self, gramps_id): self.dbase.add_note(note, self.trans) return note - def __loc_is_empty(self, location): + def __find_or_create_place(self, gramps_id): + """ + Finds or creates a place based on the Gramps ID. If the ID is + already used (is in the db), we return the item in the db. Otherwise, + we create a new place, assign the handle and Gramps ID. """ - Determines whether a location is empty. + place = Place() + intid = self.lid2id.get(gramps_id) + if self.dbase.has_place_handle(intid): + place.unserialize(self.dbase.get_raw_place_data(intid)) + else: + intid = self.__find_hndl_from_id(gramps_id, self.lid2id) + place.set_handle(intid) + place.set_gramps_id(gramps_id) + return place + + POSTAL = "postal" + + __ptype_map = { + 'Addr' : "Street", # PlaceType.STREET, + 'Subdivision' : "Street", # PlaceType.STREET, + 'Addr1' : "Street", # PlaceType.STREET, + 'Adr1' : "Street", # PlaceType.STREET, + 'Addr2' : "Locality", # PlaceType.LOCALITY, + 'Adr2' : "Locality", # PlaceType.LOCALITY, + 'State/province': "State", # PlaceType.STATE, + 'Area code' : POSTAL, + 'Post code' : POSTAL, + 'Zipcode' : POSTAL, + 'Zip code' : POSTAL, } - @param location: The current location - @type location: gen.lib.Location - @return True of False + def __parse_form(self, line): """ - if location is None: - return True - elif location.serialize() == self._EMPTY_LOC: - return True - elif location.is_empty(): - return True - return False + Provide the ability to parse GEDCOM PLAC.FORM statements for places, + and mapping the text components to PlaceType values based on the FORM + statement. - def __find_place(self, title, ptype, pref): + Note that postal codes are a special case. + + For now this assumes FORM statements are in English or the local + language; a questionable assumption. If not recognized, they will be + treated as CUSTOM grampstypes. + + It does this my mapping the text strings (separated by commas) to the + corresponding PlaceType. """ - Finds an existing place based on the title, place type and placeref. + pf_list = [] + for item in line.data.split(','): + item = item.strip() + item = self.__ptype_map.get(item.capitalize(), item) + govtype = self.place_type_dict.get(item.lower()) + if item == self.POSTAL: + ptype = item + elif govtype: + ptype = PlaceType() + ptype.pt_id = "GOV_%s" % govtype + ptype.name = item + else: + ptype = PlaceType(item) + pf_list.append(ptype) + return pf_list + + def __find_place(self, title, ptype, pref, gov=None, xref=None, + no_find=None): + """ + Finds an existing place based on one of the following, in order; + xref, if present, performs lookup as a Gramps ID. + gov, if present, performs lookup as a Gramps ID. + the title, place type and placeref + the title, placeref, and any place type (if incoming type was + default). + The 'no_find' parameter is a place to avoid finding, used with title + search only. @param title: The place title @type title: string - @param ptype: The place type + @param ptype: The PlaceType @type ptype: PlaceType @param pref: The PlaceRef.ref handle @type pref: Handle + @param gov: The GOV ID (as a Gramps ID) + @type gov: Gramps ID + @param xref: Gedcom _LOC cross reference (@P0001@) as converted to gid + @type pref: string @return gen.lib.Place - """ + @param no_find: A handle for place NOT to find + @type no_find: Handle + """ + if xref: + hndl = self.lid2id.get(xref) + if hndl: + place = self.dbase.get_place_from_handle(hndl) + return place + if gov: + place = self.dbase.get_place_from_gramps_id(gov) + if place: + return place + # search by title, type, and at least one xref for place_handle in self.place_names[title]: + if place_handle == no_find: + continue place = self.dbase.get_place_from_handle(place_handle) - if place.get_title() == title and place.get_type() == ptype: - if not pref and place.get_placeref_list() == []: - return place - for placeref in place.get_placeref_list(): - if placeref.ref == pref: + if place.get_title() == title: + for typ in place.get_types(): + if typ.is_same(ptype): + if not pref and place.get_placeref_list() == []: + return place + for placeref in place.get_placeref_list(): + if placeref.ref == pref: + return place + # search (ignoreing place types) if search place type is not known + if ptype == PlaceType.UNKNOWN: + for place_handle in self.place_names[title]: + if place_handle == no_find: + continue + place = self.dbase.get_place_from_handle(place_handle) + if place.get_title() == title: + if not pref and place.get_placeref_list() == []: return place + for placeref in place.get_placeref_list(): + if placeref.ref == pref: + return place return None def __add_place(self, event, sub_state): @@ -2948,8 +3115,12 @@ def __add_place(self, event, sub_state): place = None if sub_state.place: # we have a place - ptypes = sub_state.place_pf.get_pf() + ptypes = [] if (self.loc_loc_mode and sub_state.place.gramps_id)\ + else sub_state.place_pf places = sub_state.place.get_title().split(',') + _loc = sub_state.place.gramps_id # _LOC (mapped) + _gov = self.loc_gov.get(_loc) # _GOV from _LOC + _gov = _gov if _gov else sub_state.place_gov if len(places) == len(ptypes): # we have a hierarchy, go through it largest first prev_place_hndl = None @@ -2958,15 +3129,21 @@ def __add_place(self, event, sub_state): name = places[indx].strip() if not name: continue - if ptypes[indx] == PlaceParser.POSTAL: - sub_state.place.set_code(name) + if ptypes[indx] == self.POSTAL: + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(name) + sub_state.place.add_attribute(attr) continue title = name + \ ((', ' + title) if title else '') + # look for GOV/LOC on the basic place + xref = None if indx else _loc + gov = None if indx else _gov place = self.__find_place(title, ptypes[indx], - prev_place_hndl) + prev_place_hndl, + gov=gov, xref=xref) if place is not None: - prev_place_hndl = place.get_handle() if indx != 0: # still checking the hierarchy prev_place_hndl = place.get_handle() @@ -2975,11 +3152,22 @@ def __add_place(self, event, sub_state): # already have a place, need to merge in our stuff # we leave title alone, but set name for merge, if # different, it will end up in alt-names - sub_state.place.name.value = name + sub_state.place.set_name(PlaceName(value=name)) + sub_state.place.set_type(ptypes[indx]) if prev_place_hndl: self.__add_placeref(sub_state.place, prev_place_hndl) - place.merge(sub_state.place) + # if the place was found by _LOC or _GOV, then type + # might be different; in this case leave it as user + # had it + if place.get_type() == PlaceType.UNKNOWN: + place.set_type(ptypes[indx]) + else: + sub_state.place.set_types([]) + if gov: + place.gramps_id = gov + self.__place_merge(place, sub_state.place) + assert(place.handle) self.dbase.commit_place(place, self.trans) break elif indx != 0: @@ -2988,27 +3176,49 @@ def __add_place(self, event, sub_state): else: # a new place with all the items place = sub_state.place + if gov: + place.gramps_id = gov place.set_title(title) - place.name.value = name + place.set_name(PlaceName(value=name)) if prev_place_hndl: self.__add_placeref(place, prev_place_hndl) place.set_type(ptypes[indx]) + place.set_group(ptypes[indx].get_probable_group()) + if not place.gramps_id: + place.gramps_id = self.lid_map[""] self.dbase.add_place(place, self.trans) prev_place_hndl = place.get_handle() self.place_names[title].append(place.handle) else: # No hierarchy available, just save it. - sub_state.place.name.value = sub_state.place.title - place = self.__find_place(sub_state.place.title, - sub_state.place.place_type, None) + sub_state.place.set_name( + PlaceName(value=sub_state.place.title)) + + place = self.__find_place( + sub_state.place.title, + sub_state.place.get_type(), None, + gov=_gov, xref=_loc) if place is None: place = sub_state.place + if _gov: + place.gramps_id = _gov + if not place.gramps_id: + place.gramps_id = self.lid_map[""] + place.set_group(place.get_type().get_probable_group()) self.dbase.add_place(place, self.trans) self.place_names[place.title].append(place.handle) else: - place.merge(sub_state.place) + if(sub_state.place.get_type() == PlaceType.UNKNOWN): + sub_state.place.set_types([]) + if _gov: + place.gramps_id = _gov + self.__place_merge(place, sub_state.place) + assert(place.handle) + place.set_group(place.get_type().get_probable_group()) self.dbase.commit_place(place, self.trans) event.set_place_handle(place.get_handle()) + self.lid2id[_loc] = place.handle + if sub_state.addr_place: # ADDR was in EVEN, need to make hierarchy ptypes = sub_state.addr_pf @@ -3030,7 +3240,11 @@ def __add_place(self, event, sub_state): continue else: # already have a place, need to merge in our stuff - a_place.merge(sub_state.addr_place) + self.__place_merge(a_place, sub_state.addr_place) + assert(a_place.handle) + if a_place.group == P_G.NONE: + a_place.set_group( + a_place.get_type().get_probable_group()) self.dbase.commit_place(a_place, self.trans) continue elif indx != 0: @@ -3040,10 +3254,13 @@ def __add_place(self, event, sub_state): # a new place with all the items a_place = sub_state.addr_place a_place.set_title(title) - a_place.name.set_value(places[indx].strip()) + a_place.set_name(PlaceName(value=places[indx].strip())) if prev_place_hndl: self.__add_placeref(a_place, prev_place_hndl) a_place.set_type(ptypes[indx]) + a_place.gramps_id = self.lid_map[""] + if a_place.group == P_G.NONE: + a_place.set_group(a_place.get_type().get_probable_group()) self.dbase.add_place(a_place, self.trans) prev_place_hndl = a_place.get_handle() self.place_names[title].append(a_place.get_handle()) @@ -3055,6 +3272,109 @@ def __add_place(self, event, sub_state): self.__add_placeref(a_place, place.get_handle()) self.dbase.commit_place(a_place, self.trans) + def __place_merge(self, place, acquisition): + ''' Merge the places, similar to normal place merge, except that + names and types are merged on value only, so the result is less + restrictive. This is necessary because names and values might be + coming from _LOC and from PLAC.FORM where they are not as complete as + from _LOC. + Also assumes that one side doesn't have date/lang info at all. + + For notes, Gedcom file might contain the same note attached to a place + many times, once for each mention of the place. + + We know the acquisition note is just text, so only need to + compare that. + + @param place: a Place object + @type place: class Place + @param acquisition: a Place object + @type acquisition: class Place + ''' + name_list = place.name_list[:] + for addendum in acquisition.name_list: + for name in name_list: + if name.value == addendum.value: + if name.get_date_object().is_empty() and not name.lang: + name.date = addendum.get_date_object() + name.lang = addendum.lang + name.merge(addendum) + elif(addendum.get_date_object().is_empty() and + not addendum.lang): + name.merge(addendum) + else: + place.add_name(addendum) + break + place.add_name(addendum) + acquisition.name_list = [] + type_list = place.type_list[:] + for addendum in acquisition.type_list: + for ptype in type_list: + if ptype.is_same(addendum): + # look alike, is one definitive? + if ptype.get_date_object().is_empty(): + ptype.date = addendum.get_date_object() + ptype.merge(addendum) + elif addendum.get_date_object().is_empty(): + ptype.merge(addendum) + else: + place.add_type(addendum) + break + else: + if addendum != PlaceType.UNKNOWN: + if(place.type_list and + place.type_list[0] == PlaceType.UNKNOWN): + del place.type_list[0] + place.add_type(addendum) + acquisition.type_list = [] + if acquisition.lat and not place.lat: + place.lat = acquisition.lat + if acquisition.long and not place.long: + place.long = acquisition.long + for addendum in acquisition.note_list: + if self.dbase.has_note_handle(addendum): + add_note = self.dbase.get_note_from_handle(addendum) + else: + place.add_note(addendum) + continue + for note_h in place.note_list: + if addendum == note_h: + break # pointing to same note, skip it. + if self.dbase.has_note_handle(note_h): + note = self.dbase.get_note_from_handle(note_h) + if note.get() == add_note.get(): + # need to delete the acquisitions note + self.dbase.remove_note(addendum, self.trans) + break + else: # only executed if the inner loop did NOT break + place.add_note(addendum) + acquisition.note_list = [] + place.merge(acquisition) + + def __place_merge_full(self, phoenix, titanic): + """ + This code does the same thing as gen.merge.mergeplacequery + except it can be run locally under a transaction + """ + new_handle = phoenix.get_handle() + old_handle = titanic.get_handle() + phoenix.merge(titanic) + + self.dbase.commit_place(phoenix, self.trans) + for (class_name, handle) in self.dbase.find_backlink_handles( + old_handle): + obj = self.dbase.method("get_%s_from_handle", class_name)(handle) + assert(obj.has_handle_reference('Place', old_handle)) + obj.replace_handle_reference('Place', old_handle, new_handle) + self.dbase.method("commit_%s", class_name)(obj, self.trans) + self.dbase.remove_place(old_handle, self.trans) + # Clear titanic out of the place names list + plist = self.place_names.get(titanic.title) + if plist and titanic.handle in plist: + plist.remove(titanic.handle) + if not plist: + del self.place_names[titanic.title] + def __find_file(self, fullname, altpath): # try to find the media file fullname = fullname.replace('\\', os.path.sep) @@ -3260,17 +3580,20 @@ def _backup(self): def __check_xref(self): - def __check(_map, has_gid_func, class_func, commit_func, - gramps_id2handle, msg): + def __check(_map, objtype, class_func, gramps_id2handle, msg): for input_id, gramps_id in _map.map().items(): # Check whether an object exists for the mapped gramps_id - if not has_gid_func(gramps_id): - _handle = self.__find_hndl_from_id(gramps_id, - gramps_id2handle) + if not self.dbase.method("has_%s_gramps_id", + objtype)(gramps_id): + _handle = gramps_id2handle.get(gramps_id) + if _handle and self.dbase.method("has_%s_handle", + objtype)(_handle): + continue if msg == "FAM": make_unknown(gramps_id, self.explanation.handle, - class_func, commit_func, self.trans, - db=self.dbase) + class_func, + self.dbase.method("commit_%s", objtype), + self.trans, db=self.dbase) self.missing_references += 1 self.__add_msg(_("Error: %(msg)s '%(gramps_id)s'" " (input as @%(xref)s@) not in input" @@ -3279,7 +3602,9 @@ def __check(_map, has_gid_func, class_func, commit_func, 'xref' : input_id}) else: make_unknown(gramps_id, self.explanation.handle, - class_func, commit_func, self.trans) + class_func, + self.dbase.method("commit_%s", objtype), + self.trans, db=self.dbase) self.missing_references += 1 self.__add_msg(_("Error: %(msg)s '%(gramps_id)s'" " (input as @%(xref)s@) not in input" @@ -3291,24 +3616,20 @@ def __check(_map, has_gid_func, class_func, commit_func, self.explanation = create_explanation_note(self.dbase) self.missing_references = 0 - __check(self.pid_map, self.dbase.has_person_gramps_id, - self.__find_or_create_person, self.dbase.commit_person, + __check(self.pid_map, 'person', self.__find_or_create_person, self.gid2id, "INDI") - __check(self.fid_map, self.dbase.has_family_gramps_id, - self.__find_or_create_family, self.dbase.commit_family, + __check(self.fid_map, 'family', self.__find_or_create_family, self.fid2id, "FAM") - __check(self.sid_map, self.dbase.has_source_gramps_id, - self.__find_or_create_source, self.dbase.commit_source, + __check(self.sid_map, 'source', self.__find_or_create_source, self.sid2id, "SOUR") - __check(self.oid_map, self.dbase.has_media_gramps_id, - self.__find_or_create_media, self.dbase.commit_media, + __check(self.oid_map, 'media', self.__find_or_create_media, self.oid2id, "OBJE") - __check(self.rid_map, self.dbase.has_repository_gramps_id, - self.__find_or_create_repository, self.dbase.commit_repository, + __check(self.rid_map, 'repository', self.__find_or_create_repository, self.rid2id, "REPO") - __check(self.nid_map, self.dbase.has_note_gramps_id, - self.__find_or_create_note, self.dbase.commit_note, + __check(self.nid_map, 'note', self.__find_or_create_note, self.nid2id, "NOTE") + __check(self.lid_map, 'place', self.__find_or_create_place, + self.lid2id, "PLACE") # Check persons membership in referenced families def __input_fid(gramps_id): @@ -3600,6 +3921,8 @@ def __parse_record(self): n <> {1:1} | n <> {1:1} + | + n <> {1:1} ] This also deals with the SUBN (submission) record, of which there @@ -3636,6 +3959,8 @@ def __parse_record(self): self.__check_msgs(_("Top Level"), state, None) elif key in ("SOUR", "SOURCE"): self.__parse_source(line.token_text, 1) + elif key == "_LOC": + self.__parse_location(line) elif (line.data.startswith("SOUR ") or line.data.startswith("SOURCE ")): # A source formatted in a single line, for example: @@ -3816,6 +4141,7 @@ def __person_resn(self, line, state): """ attr = Attribute() attr.set_type((AttributeType.CUSTOM, 'RESN')) + attr.set_value(line.data.strip()) state.person.add_attribute(attr) def __person_alt_name(self, line, state): @@ -4023,12 +4349,11 @@ def __person_std_event(self, line, state): event_ref = EventRef() self.dbase.add_event(event, self.trans) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.person = state.person sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4141,7 +4466,7 @@ def __person_rnote(self, line, state): @param state: The current state @type state: CurrentState """ - self.__parse_note(line, state.person, state) + self.__parse_note(line, state.person, state) # never executed def __person_addr(self, line, state): """ @@ -4165,7 +4490,6 @@ def __person_addr(self, line, state): if self.is_ftw: self.__person_resi(line, state) return - free_form = line.data sub_state = CurrentState(level=state.level + 1) sub_state.addr = Address() @@ -4268,12 +4592,11 @@ def __person_titl(self, line, state): event.set_type(EventType.NOB_TITLE) event.set_description(line.data) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.person = state.person sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4291,7 +4614,7 @@ def __person_attr_plac(self, line, state): @param state: The current state @type state: CurrentState """ - if state.attr.get_value() == "": + if state.attr.get_value() == "": # Never executed state.attr.set_value(line.data) def __name_type(self, line, state): @@ -4656,11 +4979,10 @@ def build_lds_ord(self, state, lds_type): @param lds_type: The type of the LDS ordinance @type line: LdsOrd type """ - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.level = state.level + 1 sub_state.lds_ord = LdsOrd() sub_state.lds_ord.set_type(lds_type) - sub_state.place_pf = PlaceParser() sub_state.person = state.person state.person.lds_ord_list.append(sub_state.lds_ord) @@ -4714,7 +5036,7 @@ def __lds_form(self, line, state): @param state: The current state @type state: CurrentState """ - state.place_pf = PlaceParser(line) + state.place_pf = self.__parse_form(line) def __lds_plac(self, line, state): """ @@ -4729,11 +5051,11 @@ def __lds_plac(self, line, state): The Gedcom spec doesn't treat LDS places the same as event places, instead it just has the single line with title. """ - title = line.data + title = line.data.strip() if state.place is None: state.place = Place() state.place.set_title(title) - state.place.name.set_value(line.data) + state.place.set_name(PlaceName(value=line.data)) else: # We have previously found a PLAC self.__add_msg(_("A second PLAC ignored"), line, state) @@ -5111,12 +5433,11 @@ def __family_std_event(self, line, state): event_ref.set_role(EventRoleType.FAMILY) self.dbase.add_event(event, self.trans) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.person = state.person sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5168,12 +5489,11 @@ def __family_even(self, line, state): event.set_description(str(line.data)) self.dbase.add_event(event, self.trans) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.person = state.person sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5251,14 +5571,12 @@ def __family_slgs(self, line, state): @param state: The current state @type state: CurrentState """ - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.level = state.level + 1 sub_state.lds_ord = LdsOrd() sub_state.lds_ord.set_type(LdsOrd.SEAL_TO_SPOUSE) sub_state.place = None sub_state.family = state.family - # LDS places don't use HEAD.PLAC.FORM - sub_state.place_pf = PlaceParser() state.family.lds_ord_list.append(sub_state.lds_ord) self.__parse_level(sub_state, self.lds_parse_tbl, self.__ignore) @@ -5477,14 +5795,14 @@ def __obje(self, line, state, pri_obj): photo.set_description(path.replace('\\', '/')) full_path = os.path.abspath(path) # deal with mime types - value = mimetypes.guess_type(full_path) + value = mimetypes_guess_type(full_path) if value and value[0]: # found from filename photo.set_mime_type(value[0]) else: # get from OBJE.FILE.FORM if '/' in sub_state.form: # already has expanded mime type photo.set_mime_type(sub_state.form) else: - value = mimetypes.types_map.get('.' + sub_state.form, + value = mimetypes_types_map.get('.' + sub_state.form, _('unknown')) photo.set_mime_type(value) if sub_state.attr: @@ -5696,7 +6014,7 @@ def __event_place(self, line, state): state.place = Place() place = state.place place.set_title(line.data) - place.name.set_value(line.data) + place.set_name(PlaceName(value=line.data)) sub_state = CurrentState() sub_state.place = place @@ -5708,6 +6026,7 @@ def __event_place(self, line, state): if sub_state.place_pf: # if we found local PLAC:FORM # save to override global value state.place_pf = sub_state.place_pf + state.place_gov = sub_state.place_gov # merge notes etc into place state.place.merge(sub_state.place) @@ -5727,7 +6046,7 @@ def __place_form(self, line, state): @param state: The current state @type state: CurrentState """ - state.place_pf = PlaceParser(line) + state.place_pf = self.__parse_form(line) def __place_object(self, line, state): """ @@ -5747,6 +6066,552 @@ def __place_sour(self, line, state): """ state.place.add_citation(self.handle_source(line, state.level, state)) + def __place_fone(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the FONE (Place_Phonetic_Variation) in the PlaceNote + """ + text = "%s %s\n" % (_("Place Phonetic Variation:"), line.data) + subtext, cits = self.__date_cit_type(state) + self.__do_note(state.place, _('Place Attribute'), text + subtext) + state.place.get_name().get_citation_list().extend(cits) + + def __place_romn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the ROMN (Place_Romanized_Variation) in the PlaceNote + """ + text = "%s %s\n" % (_("Place Romanized Variation:"), line.data) + subtext, cits = self.__date_cit_type(state) + self.__do_note(state.place, _('Place Attribute'), text + subtext) + state.place.get_name().get_citation_list().extend(cits) + + def __place_chan(self, line, state): + """ Parse the Change time for the place """ + self.__parse_change(line, state.place, state.level + 1, state) + + def __place_gov(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the GOV ID in the Gramps ID + """ + state.place_gov = line.data.strip() + + def __place_subtype(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the type in ftype + """ + state.ftype = line.data.strip() + + def __place_attr(self, line, state, attrtype): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + @param attrtype: attribute type + @type attrtype: str or int + + Store the information in a Place Attribute + """ + attr = Attribute() + attr.set_type(attrtype) + text = line.data.strip() + subtext, cits = self.__date_cit_type(state, one_line=True) + attr.set_citation_list(cits) + attr.set_value(text + subtext) + state.place.add_attribute(attr) + + def __place_post(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the _post in a Place Attribute + """ + self.__place_attr(line, state, AttributeType.POSTAL) + + def __place_demo(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the _DMGD (demographical data) a Place Attribute + """ + self.__place_attr(line, state, AttributeType.DMGD) + + def __place_aidn(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the _AIDN (Administrative Identifier) in a Place Attribute + """ + self.__place_attr(line, state, AttributeType.AIDN) + + def __place_maiden(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the _MAIDENHEAD (Maidenhead_Locator) in a Place Attribute + """ + self.__place_attr(line, state, AttributeType.MAIDEN) + + def __place_loc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Deal with the Place + n+1 _LOC @P0001@ line + we will treat this as any other Ged Xref. + """ + state.place.gramps_id = self.lid_map[line.data] + + def __place_name(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the Place Name + """ + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.name = PlaceName(value=line.data.strip()) + sub_state.place = state.place # to allow citations + self.__parse_level(sub_state, self.loc_name_tbl, self.__undefined) + state.msg += sub_state.msg + state.place.add_name(sub_state.name) + if sub_state.title: + self.__do_note(state.place, _('Place Attribute'), + "%s %s\n" % (_("Name:"), sub_state.name.value) + + sub_state.title) + + def __place_name_date(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the date in the PlaceName + """ + state.name.set_date_object(line.data) + + def __place_name_lang(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the language code in the PlaceName + Gedcom specifies a set of names in English which are allowed. + This tries to reverse lookup the names to find 2 char iso codes. + It only works for those names in our _LOCALE_NAMES table, otherwise + it just stores the full text from the Gedcom file. + """ + lang = line.data.strip().capitalize() + for iso, item in _LOCALE_NAMES.items(): + if item[0] and lang in item[0]: + state.name.set_language(iso[:2]) + break + if not state.name.get_language(): + state.name.set_language(lang) + + def __place_name_abbr(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the abbreviation of the name in the place name + """ + subline = self.__chk_subordinate(state.level + 1, state, TOKEN_TYPE) + abbr = PlaceAbbrev(value=line.data.strip()) + if subline: + abbrtype = PlaceAbbrevType(subline.data.strip()) + abbr.set_type(abbrtype) + state.name.add_abbrev(abbr) + + def __place_name_namc(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the Place_Name_Addition of the name in The Note + """ + state.title += _(" Place Name Addition: ") + line.data + '\n' + + def __place_name_sour(self, line, state): + """ + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + state.name.add_citation(self.handle_source(line, state.level, state)) + + def __place_type(self, line, state): + """ + _LOC.TYPE + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + Store the Place type + """ + type_str = line.data.strip() + govtype = self.place_type_dict.get(type_str.lower()) + if govtype: + ptype = PlaceType() + ptype.pt_id = "GOV_%d" % govtype + ptype.name = type_str + else: + ptype = PlaceType(type_str) + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.event = Event() # we use this to save date/citation + self.__parse_level(sub_state, self.date_cit_type_tbl, self.__undefined) + ptype.set_date_object(sub_state.event.get_date_object()) + ptype.set_citation_list(sub_state.event.citation_list) + state.place.add_type(ptype) + + def __place_even(self, line, state): + """ + Parses the custom EVEN tag, which has the format of: + + n <> {1:1} + +1 <> {0:1} p.* + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + """ + event_ref = self.__build_event_pair(state, EventType.CUSTOM, + self.event_parse_tbl, line.data) + event_ref.set_role(EventRoleType.PLACE) + state.place.add_event_ref(event_ref) + + def __place_loc_ref(self, line, state): + """ + Parses the custom _LOC tag, which has the format of: + + n _LOC @@ {0:M} + +1 TYPE {1:1} + +1 DATE {0:1} + +1 << SOURCE_CITATION >> {0:M} + + @param line: The current line in GedLine format + @type line: GedLine + @param state: The current state + @type state: CurrentState + + This puts the 'enclosed by' (PlaceRef) into the Place.placeref_list. + Since Gramps has nowhere to store the TYPE or citation, they are put + into The Note. + """ + h_type = {'POLI' : 'Administrative', + 'RELI' : 'Religious', + 'GEOG' : 'Geographical', + 'CULT' : 'Cultural'} + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.event = PlaceRef() # we use this to save date/citation + self.__parse_level(sub_state, self.date_cit_type_tbl, self.__undefined) + state.msg += sub_state.msg + if sub_state.ftype: # Hierarchical Relationship + htype = h_type.get(sub_state.ftype.upper(), sub_state.ftype) + htype = PlaceHierType(htype) + else: + htype = PlaceHierType(PlaceHierType.ADMIN) + sub_state.event.set_type(htype) + # Initially set the PlaceRef.ref to the Gedcom xref ex:'@P0001@' + # during post processing we will fix this up to a handle + sub_state.event.ref = line.data.strip() + # Add the placeref if not duplicated + for placeref in state.place.placeref_list: + if placeref.is_equivalent(sub_state.event) != DIFFERENT: + placeref.merge(sub_state.event) + break + else: + state.place.placeref_list.append(sub_state.event) + + def __date_cit_type(self, state, one_line=False): + """ + @param state: The current state + @type state: CurrentState + @param one_line: (True) Format as a single line or + (False) do multiline and Indent the text + @type one_line: Bool + @return Returns a tuple containing the text and citation list. + @rtype tuple + + This is used for Place attributes, where we don't have a way to store + the Type and Date. + This parses lines like the following, returning the results as str. + 2 DATE {0:1} + 2 << SOURCE_CITATION >> {0:M} + 2 TYPE {1:1} + Text and Citations are returned + """ + sub_state = CurrentState() + sub_state.level = state.level + 1 + sub_state.event = Event() # we use this to save date/citation + self.__parse_level(sub_state, self.date_cit_type_tbl, self.__undefined) + text = '' + fmt = '; %s %s' if one_line else ' %s %s\n' + state.msg += sub_state.msg + if sub_state.ftype: + text += fmt % (_("Type:"), sub_state.ftype) + if not sub_state.event.get_date_object().is_empty(): + text += fmt % (_("Date:"), + complete_gedcom_date( + sub_state.event.get_date_object())) + return (text, sub_state.event.citation_list) + + def __loc_postprocess(self): + """ + Go through committed _LOC places and finish them up. + + We run through the list recursively as many times a necessary, each + time we only prepare to commit if all the place_refs have good + titles. So largest (most enclosing) places are completed first. + + If place refs are good, or there are no place_refs, the place is + 'ready'. We try to find the _LOC place in the original db committed + places. If found, we merge in to the original place. + + In some cases, (missing _LOC, or if places enclose each other, perhaps + at different times) the place may never be 'ready'. In this case we + just commit it with the best effort title. + """ + place_ready = set() # handles of places that have good enclosures + while self.locs_list: + locs_list = [] + for (handle, lid) in self.locs_list: + fnd_place = None + ready = True + place = self.dbase.get_place_from_handle(handle) + # fix up enclosure references, replaceing lid (@P0000@) with + # handles + if place.placeref_list: + for pref in place.placeref_list: + xref = pref.ref + if xref.startswith('@'): + gid = self.lid_map[xref] + ref_hndl = self.lid2id.get(gid) + if ref_hndl in place_ready: + pref.ref = ref_hndl + else: + ready = False + + if ready: + self.update() + # Need to redo PlaceRef list to avoid duplicates; editing + # of prefs above converts to handles and these might + # already be present. + ref_hndl = None + if place.placeref_list: + placeref_list = [] + for pref in place.placeref_list: + for placeref in placeref_list: + if placeref.is_equivalent(pref) != DIFFERENT: + placeref.merge(pref) + break + else: + placeref_list.append(pref) + place.placeref_list = placeref_list + # assemble a title + ref_hndl = place.placeref_list[0].ref + ref_place = self.dbase.get_place_from_handle(ref_hndl) + place.title = (place.get_name().value + ', ' + + ref_place.title) + else: + place.title = place.get_name().value + # Merge 'The Note' + self.__merge_note(place, _('Place Attribute')) + # save updated place + self.dbase.commit_place(place, self.trans) + # search for a merge match in db + fnd_place = self.__find_place(place.title, + place.get_type(), + ref_hndl, + no_find=place.handle) + if fnd_place: + self.__place_merge_full(fnd_place, place) + place = fnd_place + self.lid2id[lid] = place.handle + place_ready.add(place.handle) + else: # not ready + locs_list.append((handle, lid)) + continue + # see if list is shorter this time, making progress + if len(self.locs_list) != len(locs_list): + self.locs_list = locs_list + else: + # something is wrong, we are not making progress. Maybe a + # missing _LOC? Or places enclosed by each other? + # Just fix up xrefs and title, and save + msg = _("Gedcom issue found, missing _LOC reference: ") + for (handle, lid) in self.locs_list: + self.update() + place = self.dbase.get_place_from_handle(handle) + if place.placeref_list: + del_list = [] + for pref in place.placeref_list: + xref = pref.ref + if xref.startswith('@'): + gid = self.lid_map[xref] + pref.ref = self.lid2id.get(gid) + if not pref.ref: + # XREF to a missing _LOC + msg += xref.strip('@') + ' ' + del_list.append(pref) + for item in del_list: + place.placeref_list.remove(item) + # Merge 'The Note' + self.__merge_note(place, _('Place Attribute')) + # save updated place + self.dbase.commit_place(place, self.trans) + + # now that we finally have places with all good references, + # set titles to them + def __get_title(place_hndl, visited): + """ Assemble a title for a place and return the place """ + place = self.dbase.get_place_from_handle(place_hndl) + place.title = place.get_name().value + visited.append(place_hndl) + if place.placeref_list: + ref_hndl = place.placeref_list[0].ref + if ref_hndl in visited: + return place + place.title = (place.get_name().value + ', ' + + __get_title(ref_hndl, visited).title) + return place + + for (handle, lid) in self.locs_list: + visited = [] + self.dbase.commit_place(__get_title(handle, visited), + self.trans) + if del_list: + self.__add_msg(msg) + break + return + + def __do_note(self, obj, note_type, text): + """ + @param obj: The object containing the note + @type obj: Gramps Primary object + @param note_type: The type of note to create or edit + @type note_type: str or NoteType + @param text: The text to add to note + @type text: str or StyledText + + This creates or adds to a single note of note_type which is used to + hold information that doesn't fit anywhere else in the Gramps data. + It checks to ensure that the text was not already present. + """ + # mark record breaks with NBSP so later merge can find them + if isinstance(text, StyledText): + text.set_string(text.get_string()[:-1] + "\u00A0\n") + else: + text = text[:-1] + "\u00A0\n" + note = None + for note_hndl in obj.note_list: + note = self.dbase.get_note_from_handle(note_hndl) + if note.type == note_type: + break + note = None + if not note: + note = Note() + note.type = NoteType(note_type) + note.text += text + note.gramps_id = self.nid_map[""] + self.dbase.add_note(note, self.trans) + obj.add_note(note.handle) + else: + if str(text) not in str(note.text): + note.text += text + self.dbase.commit_note(note, self.trans) + + def __merge_note(self, obj, note_type): + """ + @param obj: The object containing the note + @type obj: Gramps Primary object + @param note_type: The type of note to create or edit + @type note_type: str or NoteType + + This finds and merges the special notes in a primary object. + It should be run after the primary objects are merged and there may be + more than one special note. + We assume the notes to be merged are ONLY attached to the primary + object. + During the merge, sub-records are delimited by a NBSP, '\n' + combination. If sub-records match, only one is kept. + """ + note_1 = None + need_commit = False + note_list = [] + for note_hndl in obj.note_list: + note = self.dbase.get_note_from_handle(note_hndl) + if note.type != note_type: + note_list.append(note_hndl) + continue + if not note_1: + note_list.append(note_hndl) + note_1 = note + continue + else: + # we have first note and a note to merge + # check for duplicate records + recs = note.text.split("\u00A0\n") + for rec in recs: + if str(rec) in str(note_1.text): + continue # duplicated record, skip + else: # something new, append to note + note_1.text += rec + "\u00A0\n" + need_commit = True + self.dbase.remove_note(note_hndl, self.trans) + if need_commit: + self.dbase.commit_note(note_1, self.trans) + obj.note_list = note_list # fix up note list for missing notes + def __place_map(self, line, state): """ n MAP @@ -5813,28 +6678,47 @@ def __event_addr(self, line, state): # of an address, better to use raw one. if not title: # indicates that no structured address provided title = addr.get_street() - state.addr_pf = [PlaceType((PlaceType.CUSTOM, _("Address")))] + ptype = PlaceType("Address") + state.addr_pf = [ptype] state.addr = [title] - sub_state.place.name.value = title + sub_state.place.set_name(PlaceName(value=title)) + sub_state.place.group = P_G(P_G.PLACE) else: # structured address provided - for item, ptype in ((addr.get_street(), PlaceType.STREET), - (addr.get_city(), PlaceType.CITY), - (addr.get_state(), PlaceType.STATE), - (addr.get_country(), PlaceType.COUNTRY)): + + def _ptype(typ): + ptd = self.place_type_dict.get(typ.lower()) + if ptd: # GOV type + ptype = PlaceType() + ptype.pt_id = "GOV_%d" % ptd + ptype.name = typ + return ptype + else: + return PlaceType(typ) + + for item, ptype in ( + (addr.get_street(), _ptype("Street")), + (addr.get_city(), _ptype("City")), + (addr.get_state(), _ptype("State")), + (addr.get_country(), _ptype("Country"))): item = item.replace("\n", " ").strip(',' + string.whitespace) if not item: continue # Don't store empties - state.addr_pf += [ptype] - state.addr += [item] - sub_state.place.name.value = state.addr[0] + state.addr_pf.append(ptype) + state.addr.append(item) + sub_state.place.set_name(PlaceName(value=state.addr[0])) sub_state.place.set_type(state.addr_pf[0]) sub_state.place.set_title(title) + # store postal code if present + if addr.postal: + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(addr.postal) + sub_state.place.add_attribute(attr) # store notes etc into place - sub_state.place.set_code(addr.postal) sub_state.place.set_note_list(addr.note_list) sub_state.place.set_citation_list(addr.citation_list) - sub_state.place.name.date = addr.date + sub_state.place.get_name().date = addr.date state.addr_place = sub_state.place def __event_privacy(self, line, state): @@ -6748,14 +7632,14 @@ def __parse_obje(self, line): if state.media.get_path() == "": self.__add_msg(_("Filename omitted"), line, state) # deal with mime types - value = mimetypes.guess_type(state.media.get_path()) + value = mimetypes_guess_type(state.media.get_path()) if value and value[0]: # found from filename state.media.set_mime_type(value[0]) else: # get from OBJE.FILE.FORM if '/' in state.form: # already has expanded mime type state.media.set_mime_type(state.form) else: - value = mimetypes.types_map.get('.' + state.form, + value = mimetypes_types_map.get('.' + state.form, _('unknown')) state.media.set_mime_type(value) # Add the default reference if no source has found @@ -7091,6 +7975,114 @@ def __optional_note(self, line, state): """ self.__parse_note(line, state.obj, state) + #---------------------------------------------------------------------- + # + # _LOC Location parsing + # + #---------------------------------------------------------------------- + + def __parse_location(self, line): + """ + 0 @@ _LOC + 1 NAME {1:M} + 2 DATE {0:1} + 2 _NAMC {0:1} + 2 ABBR {0:M} + 3 TYPE {0:1} + 2 LANG {0:1} + 2 <> {0:M} + 1 TYPE {0:M} + 2 DATE {0:1} + 2 <> {0:M} + 1 _FPOST {0:M} + 2 DATE {0:1} + 1 _POST {0:M} + 2 DATE {0:1} + 2 <> {0:M} + 1 _GOV {0:1} + 1 _FSTAE {0:1} + 1 _FCTRY {0:1} + 1 MAP {0:1} + 2 LATI {1:1} + 2 LONG {1:1} + 1 _MAIDENHEAD {0:1} + 1 EVEN [|] {0:M} + 2 <> {0:1} + 1 _LOC @@ 0:M + 2 TYPE {1:1} + 2 DATE {0:1} + 2 <> {0:M} + 1 _DMGD {0:M} + 2 DATE {0:1} + 2 <> {0:M} + 2 TYPE 1:1 + 1 _AIDN {0:M} + 2 DATE {0:1} + 2 <> {0:M} + 2 TYPE {1:1} + 1 <> {0:M} + 1 <> {0:M} + 1 <> {0:M} + 1 <> {0:1} + + We process level 0 _LOC records. The _LOC are supposed to be located + after any PLAC records. So we search by XREF (lid2id) to merge in to + already imported (commited) places. Since we now have GOV (if present) + we also do a search for that and merge into the original (non-empty) db + if found. + + Note that enclosure XREFs are NOT complete yet, as the _LOC data is not + guranteed to be in enclosed by order. So we still have to post process + the _LOC data when all _LOCs are finished (at end of GEDCOM file). + """ + place = Place() + lid = self.lid_map[line.token_text] + # The _LOC GID is from the Gedcom so we save that one + place.gramps_id = lid + + state = CurrentState() + state.place = place + state.level = 1 + state.event = Event() + self.__parse_level(state, self._loc_tbl, self.__ignore) + + self.__check_msgs( + "%s %s %s" % (_("Place"), _("Gramps ID"), place.get_gramps_id()), + state, place) + # try to find by commited place lid + fnd_place = None + hndl = self.lid2id.get(lid) + if hndl: + # We have a place already in db + fnd_place = self.dbase.get_place_from_handle(hndl) + # + if not place.get_name().is_empty(): # if _LOC has a name + # _LOC names take precedence + fnd_place.set_name(place.get_name()) + elif state.place_gov: + # we might still have a match in the db by gov + fnd_place = self.dbase.get_place_from_gramps_id(state.place_gov) + if fnd_place: + # merge in this record + self.__place_merge(fnd_place, place) + place = fnd_place + else: + # completely new place, so need a handle + place.handle = create_id() + + # make sure there is a place name + if place.get_name().is_empty(): + place.add_name(PlaceName(value=_("Unknown"))) + if state.place_gov: # override gramps_id with gov + place.gramps_id = state.place_gov + place.set_group(place.get_type().get_probable_group()) + self.dbase.commit_place(place, self.trans) + # update lid to handle + self.lid2id[lid] = place.handle + # save place for post processing if operating in _LOC/_LOC mode + if self.loc_loc_mode: + self.locs_list.append((place.handle, lid)) + #---------------------------------------------------------------------- # # HEAD parsing @@ -7449,7 +8441,7 @@ def __header_place_form(self, line, state): @param state: The current state @type state: CurrentState """ - self.place_parser.parse_form(line) + self.place_pf = self.__parse_form(line) def __header_date(self, line, state): """ @@ -7792,15 +8784,14 @@ def __build_event_pair(self, state, event_type, event_map, description): event.set_description(description) self.dbase.add_event(event, self.trans) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.level = state.level + 1 sub_state.event_ref = event_ref sub_state.event = event sub_state.person = state.person - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) - if(description == 'Y' and event.date.is_empty() and + if(description == 'Y' and event.get_date_object().is_empty() and event.type == EventType.BIRTH and not event.place): event.set_description(_("No Date Information")) state.msg += sub_state.msg @@ -7823,12 +8814,11 @@ def __build_family_event_pair(self, state, event_type, event_map, self.dbase.add_event(event, self.trans) - sub_state = CurrentState() + sub_state = CurrentState(place_pf=self.place_pf) sub_state.family = state.family sub_state.level = state.level + 1 sub_state.event = event sub_state.event_ref = event_ref - sub_state.place_pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) state.msg += sub_state.msg @@ -8006,13 +8996,20 @@ def __is_xref_value(value): """ return value and value[0] == '@' - def __init__(self, ifile): + def __init__(self, ifile, dbase): self.ifile = ifile self.famc = defaultdict(list) self.fams = defaultdict(list) self.enc = "" self.pcnt = 0 self.lcnt = 0 + self.place_type_dict = {} # stores _GOVTYPE values for pass two + self.lid_map = IdMapper( + dbase.has_place_gramps_id, + dbase.find_next_place_gramps_id, + dbase.pid2user_format) + self.loc_loc_mode = False # file uses _LOC/_LOC style of place tree + self.loc_gov = {} # _LOC (key), to _GOV (data) dict def __detect_file_decoder(self, input_file): """ @@ -8028,7 +9025,7 @@ def __detect_file_decoder(self, input_file): line = input_file.read(2) if line == b"\xef\xbb": input_file.read(1) - self.enc = "utf_8_sig" + self.enc = "UTF_8_SIG" return TextIOWrapper(input_file, encoding='utf_8_sig', errors='replace', newline=None) elif line == b"\xff\xfe" or line == b"\xfe\xff": @@ -8043,7 +9040,7 @@ def __detect_file_decoder(self, input_file): else: input_file.seek(0) return TextIOWrapper(input_file, encoding='utf-8', - errors='replace', newline=None) + errors='surrogateescape', newline=None) def parse(self): """ @@ -8052,6 +9049,8 @@ def parse(self): current_family_id = "" reader = self.__detect_file_decoder(self.ifile) + _type = '' # stores Level 1 TYPE value; works for _LOC processing + _loc = '' # stores Level 0 _LOC XREF for line in reader: # Scan for a few items, keep counts. Also look for actual CHAR @@ -8073,16 +9072,51 @@ def parse(self): if level == 0 and key[0] == '@': if value in ("FAM", "FAMILY"): current_family_id = key.strip()[1:-1] + continue elif value in ("INDI", "INDIVIDUAL"): self.pcnt += 1 - elif key in ("HUSB", "HUSBAND", "WIFE") and \ + continue + elif value == "_LOC": + # establish _LOC to id early, and hold for possible _GOV + _loc = self.lid_map[key] + continue + elif level == 1: + if key == "_LOC": + self.loc_loc_mode = True # GEDCOM uses _LOC/_LOC feature + continue + elif key == "_GOV": # _GOV id attached to level 0 _LOC + self.loc_gov[_loc] = value + continue + elif key == "TYPE": # TYPE attached to level 0 _LOC + _type = value.lower() + continue + if key in ("HUSB", "HUSBAND", "WIFE") and \ self.__is_xref_value(value): self.fams[value[1:-1]].append(current_family_id) elif key in ("CHIL", "CHILD") and self.__is_xref_value(value): self.famc[value[1:-1]].append(current_family_id) elif key == 'CHAR' and not self.enc: assert isinstance(value, str) - self.enc = value + self.enc = value.upper() + elif key == '_GOVTYPE': + # we need PlaceType by _GOVTYPE early to avoid problems with + # similar TYPE values, so store them in this pass + # Unfortunately, TYPE values can be in other languages... + byte_value = _type.encode('utf-8', errors='surrogateescape') + if '1252' in self.enc: + # if file was actually cp1252 we encoded it wrong as utf8 + _type = byte_value.decode('cp1252', + errors='surrogateescape') + elif not ('UTF' in self.enc or 'UNICODE' == self.enc): + # if file was actually latin1 we encoded it wrong as utf8 + # assume that no one uses Ansel and Gedcom L together + _type = byte_value.decode('latin1', + errors='surrogateescape') + try: + # GOV types + self.place_type_dict[_type] = int(value) + except (ValueError, TypeError): + pass LOG.debug("parse pcnt %d", self.pcnt) LOG.debug("parse famc %s", dict(self.famc)) LOG.debug("parse fams %s", dict(self.fams)) @@ -8104,14 +9138,14 @@ def get_encoding(self): """ Return the detected encoding """ - return self.enc.upper() + return self.enc def set_encoding(self, enc): """ Forces the encoding """ assert isinstance(enc, str) - self.enc = enc + self.enc = enc.upper() def get_person_count(self): """ @@ -8125,6 +9159,17 @@ def get_line_count(self): """ return self.lcnt + def get_lid_map(self): + """ + Return the xref to gid map class table + """ + return self.lid_map + + def get_place_type_dict(self): + """ + Return the _GOVTYPE place type text to int dict + """ + return self.place_type_dict #------------------------------------------------------------------------- @@ -8171,6 +9216,37 @@ def make_gedcom_date(subdate, calendar, mode, quality): return retval +def complete_gedcom_date(date): + """ + Convert a Gramps date structure into a GEDCOM compatible date. + """ + start = date.get_start_date() + if start != Date.EMPTY: + cal = date.get_calendar() + mod = date.get_modifier() + quality = date.get_quality() + if quality in DATE_QUALITY: + qual_text = DATE_QUALITY[quality] + " " + else: + qual_text = "" + if mod == Date.MOD_SPAN: + val = "%sFROM %s TO %s" % ( + qual_text, + make_gedcom_date(start, cal, mod, None), + make_gedcom_date(date.get_stop_date(), cal, mod, None)) + elif mod == Date.MOD_RANGE: + val = "%sBET %s AND %s" % ( + qual_text, + make_gedcom_date(start, cal, mod, None), + make_gedcom_date(date.get_stop_date(), cal, mod, None)) + else: + val = make_gedcom_date(start, cal, mod, quality) + return val + elif date.get_text(): + return date.get_text() + return "" + + def __build_date_string(day, mon, year, bce, mmap): """ Build a date string from the supplied information. From 6594b483370ceab2c1ed7cb82d3e120f6cc1a260 Mon Sep 17 00:00:00 2001 From: prculley Date: Thu, 28 Sep 2017 08:32:36 -0500 Subject: [PATCH 05/26] GEPS045 Gedcom L Export support --- data/tests/1024px-Texas_flag_map.svg.png | Bin 0 -> 40413 bytes data/tests/exp_sample.gramps | 545 ++++++++++++++---- data/tests/exp_sample_ged.ged | 699 +++++++++++++++++++++-- gramps/plugins/export/exportgedcom.py | 247 ++++++-- 4 files changed, 1320 insertions(+), 171 deletions(-) create mode 100644 data/tests/1024px-Texas_flag_map.svg.png diff --git a/data/tests/1024px-Texas_flag_map.svg.png b/data/tests/1024px-Texas_flag_map.svg.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a1b59a48913d54e57bbfd8ff638c48a1b31017 GIT binary patch literal 40413 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 - - + +
- + Alex Roitman,,, Not Provided @@ -12,6 +12,93 @@ {DATA_DIR}/tests
+ + + Place + + + Place + + + Place + + + Country + + + Region + + + Country + + + Place + + + Place + + + Country + Region + + + + Country + + + Region + + + Region + + + Place + + + Region + + + Place + + + + Region + + + Region + + + Region + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Place + + + Building + + + @@ -34,10 +121,10 @@ Birth of Keith Lloyd Smith - + Birth - + Birth of Hans Peter Smith @@ -53,10 +140,10 @@ Birth of Hanna Smith - + Birth - + Birth of Herman Julius Nielsen @@ -75,10 +162,10 @@ Birth of Marjorie Lee Smith - + Birth - + Birth of Gus Smith @@ -87,10 +174,10 @@ Death of Gus Smith - + Birth - + Birth of Jennifer Anderson @@ -99,10 +186,10 @@ Death of Jennifer Anderson - + Birth - + Birth of Lillie Harriet Jones @@ -137,10 +224,10 @@ Christening of Amber Marie Smith - + Birth - + Birth of Carl Emil Smith @@ -150,16 +237,16 @@ Death of Carl Emil Smith - + Birth - + Birth of Hjalmar Smith - + Death - + Death of Hjalmar Smith @@ -180,10 +267,10 @@ Baptism of Martin Smith - + Birth - + Birth of Astrid Shermanna Augusta Smith @@ -221,10 +308,10 @@ Birth of Marta Ericsdotter - + Birth - + Birth of Kirsti Marie Smith @@ -245,10 +332,10 @@ Birth of Anna Streiffert - + Death - + Death of Anna Streiffert @@ -268,10 +355,10 @@ Birth of Magnes Smith - + Death - + Death of Magnes Smith @@ -431,10 +518,10 @@ Birth of Ingar Smith - + Birth - + Birth of Hjalmar Smith @@ -443,10 +530,10 @@ Death of Hjalmar Smith - + Baptism - + Baptism of Hjalmar Smith @@ -478,10 +565,10 @@ Marriage of Eric Lloyd Smith and Darcy Horne - + Marriage - + Marriage of Magnes Smith and Anna Streiffert @@ -508,10 +595,10 @@ Marriage of Martin Smith and Kerstina Hansdotter - + Marriage - + Marriage of Gustaf Smith, Sr. and Anna Hansdotter @@ -519,10 +606,10 @@ Marriage of Edwin Willard and Kirsti Marie Smith - + Marriage - + Marriage of Herman Julius Nielsen and Astrid Shermanna Augusta Smith @@ -624,8 +711,9 @@ - + Death + LossOfMojo @@ -636,6 +724,12 @@ GainOfMojo + + History + + + Lisbon Treaty + @@ -1103,14 +1197,14 @@ リチミシキスイミ - + M The Tester - + @@ -1436,6 +1530,37 @@ + + widipedia + 2 + + + + GeoNames ID: 2614553 + 2 + + + + None, just testing + 2 + + + + + Cit for test purposes + 2 + + + + GeoNames ID: 2267056 + 2 + + + + GeoNames ID: 2264397 + 2 + + @@ -1478,173 +1603,364 @@ The tester Published when the test was written TST - + + + + GeoNames + Marc Wick + - + Löderup, Malmöhus Län, Sweden + - + Sparks, Washoe Co., NV + - + San Francisco, San Francisco Co., CA + - - Rønne, Bornholm, Denmark - - - + Gladsax, Kristianstad Län, Sweden + - + Reno, Washoe Co., NV + - + Hayward, Alameda Co., CA + - + Community Presbyterian Church, Danville, CA + - + Sweden + - + Grostorp, Kristianstad Län, Sweden + - + Copenhagen, Denmark + - + Hoya/Jona/Hoia, Sweden + - + Simrishamn, Kristianstad Län, Sweden + - + Fremont, Alameda Co., CA + - + Denver, Denver Co., CO + - + - + Sacramento, Sacramento Co., CA - + + - + Santa Rosa, Sonoma Co., CA + - + San Jose, Santa Clara Co., CA + - + UC Berkeley + - + Smestorp, Kristianstad Län, Sweden + - + Tommarp, Kristianstad Län, Sweden + - - Rønne Bornholm, Denmark - - - + Woodland, Yolo Co., CA + - + San Ramon, Conta Costa Co., CA + - + Denver Co., Colorado, USA - + + - + Colorado, USA - + + - + United States of America + + - + Sacramento Co., California, USA - + + - + California, USA - + + - + 123 High St, Cleveland, Cuyahoga, Ohio, USA, 44140 - 44140 - - + + + + + + - + 123 Main St., Winslow, PA, 12345 - + + + + + - + Cleveland, Cuyahoga, Ohio, USA - - + + + + + - - USA - - - + Cuyahoga, Ohio, USA - + + - + Ohio, USA - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Portugal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1652,6 +1968,9 @@
+ + + @@ -1723,6 +2042,13 @@ Library + + www.geonames.org + Web site + + + + @@ -1826,5 +2152,24 @@ Only one phone number supported Line 9: A bad photo for sure + + Timeline +French Texas 1684–1689 +Spanish Texas 1690–1821 +Mexican Texas 1821–1836 +Republic of Texas 1836–1845 +Statehood 1845–1860 +Civil War Era 1861–1865 +Reconstruction 1865–1899 + + + + GeoNames was founded by Marc Wick. You can reach him at marc@geonames.org +GeoNames is a project of Unxos GmbH, Weingartenstrasse 8, 8708 Männedorf, Switzerland. +This work is licensed under a Creative Commons Attribution 3.0 License + + diff --git a/data/tests/exp_sample_ged.ged b/data/tests/exp_sample_ged.ged index 8af8253d99a..1491ccee60b 100644 --- a/data/tests/exp_sample_ged.ged +++ b/data/tests/exp_sample_ged.ged @@ -32,10 +32,12 @@ 2 TYPE Birth of Anna Hansdotter 2 DATE 2 OCT 1864 2 PLAC Löderup, Malmöhus Län, Sweden +3 _LOC @P0000@ 1 DEAT 2 TYPE Death of Anna Hansdotter 2 DATE 29 SEP 1945 2 PLAC Sparks, Washoe Co., NV +3 _LOC @P0001@ 1 FAMS @F0003@ 1 ASSO @I0038@ 2 RELA Friend @@ -53,6 +55,7 @@ 2 TYPE Birth of Keith Lloyd Smith 2 DATE 11 AUG 1966 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMC @F0008@ 2 PEDI birth 1 CHAN @@ -67,12 +70,13 @@ 2 TYPE Birth of Amber Marie Smith 2 DATE 12 APR 1998 2 PLAC Hayward, Alameda Co., CA +3 _LOC @P0006@ 1 CHR 2 TYPE Christening of Amber Marie Smith 2 DATE 26 APR 1998 2 PLAC Community Presbyterian Church, Danville, CA -2 ADDR -3 ADR2 Community Presbyterian Church, Danville, CA +3 FORM Locality +3 _LOC @P0007@ 1 FAMC @F0013@ 2 PEDI birth 1 CHAN @@ -87,10 +91,16 @@ 2 TYPE Birth of Magnes Smith 2 DATE 6 OCT 1858 2 PLAC Simrishamn, Kristianstad Län, Sweden +3 _LOC @P0012@ 1 DEAT 2 TYPE Death of Magnes Smith 2 DATE 20 FEB 1910 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 FAMC @F0002@ 2 PEDI birth 1 FAMS @F0011@ @@ -106,6 +116,7 @@ 2 TYPE Birth of Ingeman Smith 2 DATE 29 JAN 1826 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMC @F0000@ 2 PEDI birth 1 CHAN @@ -120,12 +131,13 @@ 2 TYPE Birth of Mason Michael Smith 2 DATE 26 JUN 1996 2 PLAC Hayward, Alameda Co., CA +3 _LOC @P0006@ 1 CHR 2 TYPE Christening of Mason Michael Smith 2 DATE 10 JUL 1996 2 PLAC Community Presbyterian Church, Danville, CA -2 ADDR -3 ADR2 Community Presbyterian Church, Danville, CA +3 FORM Locality +3 _LOC @P0007@ 1 FAMC @F0013@ 2 PEDI birth 1 CHAN @@ -152,6 +164,7 @@ 2 TYPE Birth of Ingar Smith 2 DATE AFT 1823 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMC @F0000@ 2 PEDI birth 1 CHAN @@ -166,18 +179,30 @@ 2 TYPE Birth of Hjalmar Smith 2 DATE 7 APR 1895 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Hjalmar Smith 2 DATE 26 JUN 1975 2 PLAC Reno, Washoe Co., NV +3 _LOC @P0005@ 1 BAPM 2 TYPE Baptism of Hjalmar Smith 2 DATE 3 JUN 1895 -2 PLAC Rønne Bornholm, Denmark +2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 EVEN 2 TYPE Immi 2 DATE 14 NOV 1912 2 PLAC Copenhagen, Denmark +3 _LOC @P0010@ 1 FAMC @F0003@ 2 PEDI birth 1 FAMS @F0006@ @@ -194,6 +219,7 @@ 2 TYPE Birth of Emil Smith 2 DATE 27 SEP 1860 2 PLAC Simrishamn, Kristianstad Län, Sweden +3 _LOC @P0012@ 1 FAMC @F0002@ 2 PEDI birth 1 CHAN @@ -208,6 +234,11 @@ 2 TYPE Birth of Hans Peter Smith 2 DATE 17 APR 1904 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 2 SOUR @S0002@ 3 PAGE 22 6 3 DATA @@ -220,10 +251,12 @@ 2 TYPE Death of Hans Peter Smith 2 DATE 29 JAN 1977 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 BURI 2 TYPE In cemetary 2 DATE 5 FEB 1977 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 2 SOUR @S0004@ 3 QUAY 2 3 DATA @@ -244,6 +277,7 @@ 2 TYPE Birth of Hanna Smith 2 DATE 29 JAN 1821 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMC @F0000@ 2 PEDI birth 1 CHAN @@ -258,6 +292,11 @@ 2 TYPE Birth of Herman Julius Nielsen 2 DATE 31 AUG 1889 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Herman Julius Nielsen 2 DATE 1945 @@ -286,6 +325,7 @@ 2 TYPE Birth of Marjorie Lee Smith 2 DATE 4 NOV 1934 2 PLAC Reno, Washoe Co., NV +3 _LOC @P0005@ 1 FAMC @F0006@ 2 PEDI birth 1 CHAN @@ -300,10 +340,16 @@ 2 TYPE Birth of Gus Smith 2 DATE 11 SEP 1897 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Gus Smith 2 DATE 21 OCT 1963 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMC @F0003@ 2 PEDI birth 1 FAMS @F0007@ @@ -319,10 +365,16 @@ 2 TYPE Birth of Jennifer Anderson 2 DATE 5 NOV 1907 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Jennifer Anderson 2 DATE 29 MAY 1985 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMS @F0014@ 1 CHAN 2 DATE 21 DEC 2007 @@ -336,6 +388,11 @@ 2 TYPE Birth of Lillie Harriet Jones 2 DATE 2 MAY 1910 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Lillie Harriet Jones 2 DATE 26 JUN 1990 @@ -352,6 +409,7 @@ 2 TYPE Birth of John Hjalmar Smith 2 DATE 30 JAN 1932 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMC @F0006@ 2 PEDI birth 1 FAMS @F0012@ @@ -368,6 +426,7 @@ 2 TYPE Birth of Eric Lloyd Smith 2 DATE 28 AUG 1963 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 ADOP Y 2 FAMC @F0008@ 3 ADOP BOTH @@ -386,10 +445,16 @@ 2 TYPE Birth of Carl Emil Smith 2 DATE 20 DEC 1899 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Carl Emil Smith 2 DATE 28 JAN 1959 2 PLAC Reno, Washoe Co., NV +3 _LOC @P0005@ 2 CAUS Bad breath 1 FAMC @F0003@ 2 PEDI birth @@ -405,10 +470,20 @@ 2 TYPE Birth of Hjalmar Smith 2 DATE 31 JAN 1893 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Hjalmar Smith 2 DATE 25 SEP 1894 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 FAMC @F0003@ 2 PEDI birth 1 CHAN @@ -423,14 +498,17 @@ 2 TYPE Birth of Martin Smith 2 DATE 19 NOV 1830 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 DEAT 2 TYPE Death of Martin Smith 2 DATE BET 1899 AND 1905 2 PLAC Sweden +3 _LOC @P0008@ 1 BAPM 2 TYPE Baptism of Martin Smith 2 DATE 23 NOV 1830 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMC @F0000@ 2 PEDI birth 1 FAMS @F0002@ @@ -447,10 +525,16 @@ 2 TYPE Birth of Astrid Shermanna Augusta Smith 2 DATE 31 JAN 1889 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Astrid Shermanna Augusta Smith 2 DATE 21 DEC 1963 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMC @F0003@ 2 PEDI birth 1 FAMS @F0005@ @@ -467,18 +551,22 @@ 2 TYPE Birth of Gustaf Smith, Sr. 2 DATE 28 NOV 1862 2 PLAC Grostorp, Kristianstad Län, Sweden +3 _LOC @P0009@ 1 DEAT 2 TYPE Death of Gustaf Smith, Sr. 2 DATE BEF 23 JUL 1930 2 PLAC Sparks, Washoe Co., NV +3 _LOC @P0001@ 1 EVEN 2 TYPE Immi 2 DATE 21 MAY 1908 2 PLAC Copenhagen, Denmark +3 _LOC @P0010@ 1 CHR 2 TYPE Christening of Gustaf Smith, Sr. 2 DATE 7 DEC 1862 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMC @F0002@ 2 PEDI birth 1 FAMS @F0003@ @@ -494,6 +582,7 @@ 2 TYPE Birth of Marta Ericsdotter 2 DATE ABT 1775 2 PLAC Sweden +3 _LOC @P0008@ 1 FAMS @F0001@ 1 CHAN 2 DATE 21 DEC 2007 @@ -507,10 +596,16 @@ 2 TYPE Birth of Kirsti Marie Smith 2 DATE 15 DEC 1886 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 DEAT 2 TYPE Death of Kirsti Marie Smith 2 DATE 18 JUL 1966 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 FAMC @F0003@ 2 PEDI birth 1 FAMS @F0004@ @@ -526,6 +621,7 @@ 2 TYPE Birth of Ingeman Smith 2 DATE ABT 1770 2 PLAC Sweden +3 _LOC @P0008@ 1 FAMS @F0001@ 1 CHAN 2 DATE 21 DEC 2007 @@ -539,10 +635,16 @@ 2 TYPE Birth of Anna Streiffert 2 DATE 23 SEP 1860 2 PLAC Hoya/Jona/Hoia, Sweden +3 _LOC @P0011@ 1 DEAT 2 TYPE Death of Anna Streiffert 2 DATE 2 FEB 1927 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 FAMS @F0011@ 1 CHAN 2 DATE 21 DEC 2007 @@ -556,6 +658,7 @@ 2 TYPE Birth of Craig Peter Smith 2 DATE AFT 1966 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 CENS 2 TYPE Census of Craig Peter Smith 2 NOTE @N0000@ @@ -573,6 +676,7 @@ 2 TYPE Birth of Janice Ann Adams 2 DATE 26 AUG 1965 2 PLAC Fremont, Alameda Co., CA +3 _LOC @P0013@ 1 OCCU Retail Manager 1 _DEG 2 TYPE Business Management @@ -590,17 +694,16 @@ 2 TYPE Birth of Marjorie Ohman 2 DATE 3 JUN 1903 2 PLAC Denver, Denver Co., CO, Denver Co., Colorado, USA +3 FORM City, County, State, Country 3 MAP 4 LATI N39.7392 4 LONG W104.9903 -2 ADDR -3 CITY Denver, Denver Co., CO -3 STAE Colorado -3 CTRY USA +3 _LOC @P0014@ 1 DEAT 2 TYPE Death of Marjorie Ohman 2 DATE 22 JUN 1980 2 PLAC Reno, Washoe Co., NV +3 _LOC @P0005@ 1 FAMS @F0006@ 1 CHAN 2 DATE 21 DEC 2007 @@ -614,8 +717,8 @@ 2 TYPE Birth of Darcy Horne 2 DATE 2 JUL 1966 2 PLAC Sacramento, Sacramento Co., CA -2 ADDR -3 CITY Sacramento, Sacramento Co., CA +3 FORM City +3 _LOC @P0015@ 1 FAMS @F0010@ 1 CHAN 2 DATE 21 DEC 2007 @@ -629,6 +732,7 @@ 2 TYPE Birth of Lloyd Smith 2 DATE 13 MAR 1935 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 ADOP Y 2 FAMC @F0009@ 3 ADOP HUSB @@ -648,6 +752,7 @@ 2 TYPE Birth of Alice Paula Perkins 2 DATE 22 NOV 1933 2 PLAC Sparks, Washoe Co., NV +3 _LOC @P0001@ 1 FAMS @F0012@ 1 CHAN 2 DATE 21 DEC 2007 @@ -661,6 +766,7 @@ 2 TYPE Birth of Lars Peter Smith 2 DATE 16 SEP 1991 2 PLAC Santa Rosa, Sonoma Co., CA +3 _LOC @P0016@ 1 ADOP Y 2 FAMC @F0010@ 3 ADOP BOTH @@ -678,13 +784,16 @@ 2 TYPE Birth of Elna Jefferson 2 DATE 14 SEP 1800 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 DEAT 2 TYPE Death of Elna Jefferson 2 PLAC Sweden +3 _LOC @P0008@ 1 CHR 2 TYPE Christening of Elna Jefferson 2 DATE 16 SEP 1800 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 FAMS @F0000@ 1 CHAN 2 DATE 21 DEC 2007 @@ -699,13 +808,14 @@ 2 TYPE Birth of Edwin Michael Smith 2 DATE 24 MAY 1961 2 PLAC San Jose, Santa Clara Co., CA +3 _LOC @P0017@ 2 SOUR @S0003@ 1 OCCU Software Engineer 2 AGE 23 2 NOTE @N0001@ 1 EDUC Education of Edwin Michael Smith 2 DATE BET 1979 AND 1984 -2 PLAC UC Berkeley +2 ADDR UC Berkeley 1 _DEG 2 TYPE B.S.E.E. 2 DATE 1984 @@ -724,10 +834,12 @@ 2 TYPE Birth of Kerstina Hansdotter 2 DATE 29 NOV 1832 2 PLAC Smestorp, Kristianstad Län, Sweden +3 _LOC @P0019@ 1 DEAT 2 TYPE Death of Kerstina Hansdotter 2 DATE BEF 1908 2 PLAC Sweden +3 _LOC @P0008@ 1 FAMS @F0002@ 1 CHAN 2 DATE 21 DEC 2007 @@ -741,9 +853,11 @@ 2 TYPE Birth of Martin Smith 2 DATE BET 1794 AND 1796 2 PLAC Tommarp, Kristianstad Län, Sweden +3 _LOC @P0020@ 1 DEAT 2 TYPE Death of Martin Smith 2 PLAC Sweden +3 _LOC @P0008@ 1 FAMC @F0001@ 2 PEDI birth 1 FAMS @F0000@ @@ -759,6 +873,7 @@ 2 TYPE Birth of Marjorie Alice Smith 2 DATE 5 FEB 1960 2 PLAC San Jose, Santa Clara Co., CA +3 _LOC @P0017@ 1 FAMC @F0012@ 2 PEDI birth 1 CHAN @@ -812,17 +927,17 @@ 1 BIRT 2 DATE 29 DEC 1954 2 PLAC 123 High St, Cleveland, Cuyahoga, Ohio, USA -2 ADDR 123 High St -3 ADR1 123 High St -3 CITY Cleveland -3 STAE Ohio -3 POST 44140 -3 CTRY USA +3 FORM Street, City, County, State, Country +3 _LOC @P0029@ 2 PHON 440-871-3400 2 PHON 800-871-3400 2 EMAIL thetester@@gmail.com 2 FAX 440-123-4567 2 WWW http://thetester.com +1 DEAT +2 PLAC Houston, Harris, Texas +3 FORM City, County, State +3 _LOC @P0036@ 1 EVEN A very bad day 2 TYPE LossOfMojo 2 DATE 7 JUL 1973 @@ -850,8 +965,8 @@ 3 POST 44177 3 CTRY Cuyahoga 1 CHAN -2 DATE 29 OCT 2016 -3 TIME 16:40:40 +2 DATE 29 SEP 2017 +3 TIME 22:02:08 0 @I0045@ INDI 1 NAME Mrs /Tester/ 2 TYPE married @@ -860,7 +975,10 @@ 1 SEX F 1 RESI 2 DATE 30 DEC 1954 -2 PLAC 123 Main St., Winslow, PA, 12345 +2 ADDR 123 Main St., Winslow, PA, 12345 +2 PLAC Winslow, Pennsylvania, USA +3 FORM City, State, Country +3 _LOC @P0030@ 2 PHON 440-871-3401 2 EMAIL mrstester@@gmail.com 2 FAX 440-321-4568 @@ -896,13 +1014,11 @@ 1 RESI 2 DATE FROM 1 JAN 1964 TO 3 MAR 1970 2 PLAC Denver, Denver Co., CO, Denver Co., Colorado, USA +3 FORM City, County, State, Country 3 MAP 4 LATI N39.7392 4 LONG W104.9903 -2 ADDR -3 CITY Denver, Denver Co., CO -3 STAE Colorado -3 CTRY USA +3 _LOC @P0014@ 2 PHON 440-871-3402 2 EMAIL tomtester@@gmail.com 2 FAX 440-321-4569 @@ -910,18 +1026,17 @@ 1 RESI 2 DATE I think 1970 to 1971 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 SLGC 2 DATE ABT 1999 2 FAMC @F0016@ 2 TEMP DENVE 2 PLAC Denver, Denver Co., CO, Denver Co., Colorado, USA +3 FORM City, County, State, Country 3 MAP 4 LATI N39.7392 4 LONG W104.9903 -2 ADDR -3 CITY Denver, Denver Co., CO -3 STAE Colorado -3 CTRY USA +3 _LOC @P0014@ 1 FAMC @F0016@ 2 PEDI birth 1 SOUR @S0005@ @@ -944,6 +1059,7 @@ 2 DATE 1954 3 TIME 12:45 am 2 PLAC Fremont, Alameda Co., CA +3 _LOC @P0013@ 2 AGNC A hosptial 2 HUSB 3 AGE 47 @@ -971,13 +1087,11 @@ 2 DATE 7 JUN 1960 2 TEMP DENVE 2 PLAC Denver, Denver Co., CO, Denver Co., Colorado, USA +3 FORM City, County, State, Country 3 MAP 4 LATI N39.7392 4 LONG W104.9903 -2 ADDR -3 CITY Denver, Denver Co., CO -3 STAE Colorado -3 CTRY USA +3 _LOC @P0014@ 2 STAT QUALIFIED 1 FAMC @F0016@ 2 _FREL Adopted @@ -1055,6 +1169,7 @@ 2 TYPE Marriage of Martin Smith and Elna Jefferson 2 DATE ABT 1816 2 PLAC Gladsax, Kristianstad Län, Sweden +3 _LOC @P0004@ 1 CHIL @I0011@ 1 CHIL @I0007@ 1 CHIL @I0004@ @@ -1069,6 +1184,7 @@ 2 TYPE Marriage of Ingeman Smith and Marta Ericsdotter 2 DATE ABT 1790 2 PLAC Sweden +3 _LOC @P0008@ 1 CHIL @I0039@ 1 CHAN 2 DATE 21 DEC 2007 @@ -1092,6 +1208,11 @@ 2 TYPE Marriage of Gustaf Smith, Sr. and Anna Hansdotter 2 DATE 27 NOV 1885 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 CHIL @I0026@ 1 CHIL @I0023@ 1 CHIL @I0021@ @@ -1118,6 +1239,11 @@ 2 TYPE Marriage of Herman Julius Nielsen and Astrid Shermanna Augusta Smith 2 DATE 30 NOV 1912 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 CHIL @I0042@ 1 CHAN 2 DATE 12 JUN 2016 @@ -1129,6 +1255,7 @@ 2 TYPE Marriage of Hjalmar Smith and Marjorie Ohman 2 DATE 31 OCT 1927 2 PLAC Reno, Washoe Co., NV +3 _LOC @P0005@ 1 CHIL @I0018@ 1 CHIL @I0014@ 1 CHAN @@ -1150,6 +1277,7 @@ 2 TYPE Marriage of Lloyd Smith and Janis Elaine Green 2 DATE 10 AUG 1958 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 CHIL @I0019@ 1 CHIL @I0001@ 1 CHIL @I0029@ @@ -1171,6 +1299,7 @@ 2 TYPE Marriage of Eric Lloyd Smith and Darcy Horne 2 DATE 12 JUL 1986 2 PLAC Woodland, Yolo Co., CA +3 _LOC @P0022@ 1 CHIL @I0035@ 1 CHAN 2 DATE 21 DEC 2007 @@ -1182,6 +1311,11 @@ 2 TYPE Marriage of Magnes Smith and Anna Streiffert 2 DATE 24 AUG 1884 2 PLAC Rønne, Bornholm, Denmark +3 FORM City, Amt (dänisches), kingdom +3 MAP +4 LATI N55.1 +4 LONG E14.7 +3 _LOC @GEO2614553@ 1 CHAN 2 DATE 21 DEC 2007 3 TIME 07:35:26 @@ -1192,6 +1326,7 @@ 2 TYPE Marriage of John Hjalmar Smith and Alice Paula Perkins 2 DATE 4 JUN 1954 2 PLAC Sparks, Washoe Co., NV +3 _LOC @P0001@ 2 SOUR @S0000@ 1 CHIL @I0040@ 1 CHIL @I0037@ @@ -1205,10 +1340,12 @@ 2 TYPE Marriage of Edwin Michael Smith and Janice Ann Adams 2 DATE 27 MAY 1995 2 PLAC San Ramon, Conta Costa Co., CA +3 _LOC @P0023@ 1 ENGA 2 TYPE Engagement of Edwin Michael Smith and Janice Ann Adams 2 DATE 5 OCT 1994 2 PLAC San Francisco, San Francisco Co., CA +3 _LOC @P0002@ 1 CHIL @I0005@ 1 CHIL @I0002@ 1 CHAN @@ -1227,8 +1364,8 @@ 2 DATE 22 FEB 2000 2 TEMP DENVE 2 PLAC Community Presbyterian Church, Danville, CA -2 ADDR -3 ADR2 Community Presbyterian Church, Danville, CA +3 FORM Locality +3 _LOC @P0007@ 2 STAT CLEARED 1 MARR 2 HUSB @@ -1256,6 +1393,465 @@ 1 CHAN 2 DATE 29 OCT 2016 3 TIME 16:27:59 +0 @GEO2264397@ _LOC +1 NAME Portugal +2 LANG English +2 ABBR PT +3 TYPE ISO3166 +1 NAME Portuguese Republic +2 ABBR PT +3 TYPE ISO3166 +1 NAME Lusitania +2 ABBR PT +3 TYPE ISO3166 +1 NAME Portugal +2 LANG German +1 NAME Portugal +2 LANG French +1 NAME Portuguese Republic +2 LANG English +2 ABBR PT +3 TYPE ISO3166 +1 NAME Republic of Portugal +2 ABBR PT +3 TYPE ISO3166 +1 NAME Portugal +1 NAME República Portuguesa +1 TYPE republic +2 _GOVTYPE 56 +1 MAP +2 LATI N39.6945 +2 LONG W8.13057 +1 _LOC @object_1176980@ +2 DATE AFT 1986 +2 TYPE POLI +1 SOUR @S0008@ +2 PAGE GeoNames ID: 2264397 +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 17:00:33 +0 @GEO2614553@ _LOC +1 NAME Rønne +1 NAME Rønne +2 LANG German +1 NAME Rønne +2 LANG English +1 NAME Rønne +2 LANG Danish +1 TYPE City +1 TYPE place39 +2 _GOVTYPE 39 +1 _POST 3700 +1 _DMGD 13890 +2 TYPE Population +1 MAP +2 LATI N55.1 +2 LONG E14.7 +1 _LOC @object_364181@ +2 TYPE POLI +1 SOUR @S0008@ +2 PAGE GeoNames ID: 2614553 +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 16:25:39 +0 @P0000@ _LOC +1 NAME Löderup, Malmöhus Län, Sweden +1 CHAN +2 DATE 12 JUN 2016 +3 TIME 14:30:26 +0 @P0001@ _LOC +1 NAME Sparks, Washoe Co., NV +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0002@ _LOC +1 NAME San Francisco, San Francisco Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0003@ _LOC +1 NAME Pennsylvania +1 NAME PA +1 TYPE State +1 _LOC @P0026@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:29:02 +0 @P0004@ _LOC +1 NAME Gladsax, Kristianstad Län, Sweden +1 CHAN +2 DATE 12 JUN 2016 +3 TIME 14:30:26 +0 @P0005@ _LOC +1 NAME Reno, Washoe Co., NV +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0006@ _LOC +1 NAME Hayward, Alameda Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0007@ _LOC +1 NAME Community Presbyterian Church, Danville, CA +1 TYPE Locality +1 CHAN +2 DATE 29 OCT 2016 +3 TIME 15:25:02 +0 @P0008@ _LOC +1 NAME Sweden +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0009@ _LOC +1 NAME Grostorp, Kristianstad Län, Sweden +1 CHAN +2 DATE 12 JUN 2016 +3 TIME 14:30:26 +0 @P0010@ _LOC +1 NAME Copenhagen, Denmark +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0011@ _LOC +1 NAME Hoya/Jona/Hoia, Sweden +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0012@ _LOC +1 NAME Simrishamn, Kristianstad Län, Sweden +1 CHAN +2 DATE 12 JUN 2016 +3 TIME 14:29:48 +0 @P0013@ _LOC +1 NAME Fremont, Alameda Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0014@ _LOC +1 NAME Denver, Denver Co., CO +1 TYPE City +1 MAP +2 LATI N39.7392 +2 LONG W104.9903 +1 _LOC @P0024@ +2 TYPE POLI +1 CHAN +2 DATE 4 JUN 2016 +3 TIME 21:16:23 +0 @P0015@ _LOC +1 NAME Sacramento, Sacramento Co., CA +1 TYPE City +1 _LOC @P0027@ +2 DATE 4 JUN 2016 +2 TYPE POLI +1 CHAN +2 DATE 4 JUN 2016 +3 TIME 21:21:24 +0 @P0016@ _LOC +1 NAME Santa Rosa, Sonoma Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0017@ _LOC +1 NAME San Jose, Santa Clara Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0018@ _LOC +1 NAME UC Berkeley +1 TYPE Address +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 22:12:28 +0 @P0019@ _LOC +1 NAME Smestorp, Kristianstad Län, Sweden +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0020@ _LOC +1 NAME Tommarp, Kristianstad Län, Sweden +1 CHAN +2 DATE 12 JUN 2016 +3 TIME 14:30:26 +0 @P0021@ _LOC +1 NAME Winslow +1 TYPE City +1 _LOC @P0003@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:28:12 +0 @P0022@ _LOC +1 NAME Woodland, Yolo Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0023@ _LOC +1 NAME San Ramon, Conta Costa Co., CA +1 CHAN +2 DATE 22 MAY 2016 +3 TIME 15:13:17 +0 @P0024@ _LOC +1 NAME Denver Co. +1 TYPE County +1 _LOC @P0025@ +2 TYPE POLI +1 CHAN +2 DATE 7 JUN 2016 +3 TIME 16:18:24 +0 @P0025@ _LOC +1 NAME Colorado +1 TYPE State +1 _LOC @P0026@ +2 TYPE POLI +1 CHAN +2 DATE 7 JUN 2016 +3 TIME 16:18:00 +0 @P0026@ _LOC +1 NAME USA +1 NAME United States of America +1 TYPE Country +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:38:06 +0 @P0027@ _LOC +1 NAME Sacramento Co. +1 TYPE County +1 _LOC @P0028@ +2 DATE 1 JUN 2016 +2 TYPE POLI +1 CHAN +2 DATE 7 JUN 2016 +3 TIME 16:18:44 +0 @P0028@ _LOC +1 NAME California +1 TYPE State +1 _LOC @P0026@ +2 DATE 4 JUN 2016 +2 TYPE POLI +1 CHAN +2 DATE 7 JUN 2016 +3 TIME 16:17:48 +0 @P0029@ _LOC +1 NAME 123 High St +2 DATE AFT 1952 +2 LANG English +1 TYPE Street +1 _POST 44140 +1 _LOC @P0034@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:23:27 +0 @P0030@ _LOC +1 NAME 123 Main St., Winslow, PA, 12345 +2 DATE AFT 1950 +2 LANG English +1 TYPE Address +1 _LOC @P0021@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:28:13 +0 @P0031@ _LOC +1 NAME Texas +1 TYPE State +1 _LOC @P0026@ +2 DATE AFT 1845 +2 TYPE POLI +1 _LOC @P0037@ +2 DATE FROM 1836 TO 1845 +2 TYPE POLI +1 _LOC @P0038@ +2 DATE FROM 1690 TO 1821 +2 TYPE POLI +1 OBJE @O0001@ +1 NOTE @N0020@ +1 SOUR @S0007@ +2 PAGE widipedia +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:43:23 +0 @P0032@ _LOC +1 NAME Ohio +1 TYPE State +1 _LOC @P0026@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:29:42 +0 @P0033@ _LOC +1 NAME Cuyahoga +1 TYPE County +1 _LOC @P0032@ +2 TYPE POLI +1 CHAN +2 DATE 29 AUG 2016 +3 TIME 19:51:48 +0 @P0034@ _LOC +1 NAME Cleveland +2 DATE AFT 1796 +2 LANG English +1 TYPE City +1 _LOC @P0033@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:25:23 +0 @P0035@ _LOC +1 NAME Harris +1 TYPE County +1 _LOC @P0031@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:36:37 +0 @P0036@ _LOC +1 NAME Houston +1 TYPE City +1 _LOC @P0035@ +2 TYPE POLI +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:36:40 +0 @P0037@ _LOC +1 NAME Republic of Texas +1 TYPE Country +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:39:53 +0 @P0038@ _LOC +1 NAME Mexico +1 TYPE Country +1 CHAN +2 DATE 29 SEP 2017 +3 TIME 21:40:58 +0 @object_1176980@ _LOC +1 NAME European Union +2 DATE AFT 2009 +2 LANG English +2 SOUR @S0008@ +3 PAGE None, just testing +2 ABBR EU +3 TYPE Abbreviation +1 NAME Europese Unie +2 LANG Dutch +1 NAME Euroopan Yhteisö +2 DATE BEF 2009 +2 LANG Finnish +1 NAME Europäische Union +2 DATE AFT 2009 +2 LANG German +1 NAME Union européenne +2 LANG French +1 NAME Euroopan Unioni +2 LANG Finnish +1 NAME Aontais Eorpaigh +2 LANG Gaelic +1 NAME Europäische Gemeinschaft +2 DATE BEF 2009 +2 LANG German +1 NAME Unión Europea +2 LANG Spanish +1 NAME European Community +2 DATE FROM 1992 TO 2009 +2 LANG English +1 TYPE confederation +2 _GOVTYPE 71 +2 SOUR @S0008@ +3 PAGE Cit for test purposes +3 DATA +4 DATE JUN 2019 +1 _GOV object_1176980 +1 MAP +2 LATI N50.342813 +2 LONG E10.784506 +1 EVEN Lisbon Treaty +2 TYPE History +2 DATE 2009 +2 PLAC Lisbon, Portugal, European Community +3 FORM town, republic, confederation +3 MAP +4 LATI N38.72564 +4 LONG W9.150028 +3 _LOC @object_1177032@ +3 _GOV object_1177032 +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 16:52:08 +0 @object_1177032@ _LOC +1 NAME Lisbon +2 LANG English +1 NAME Lisboa +1 NAME Distrito de Lisboa +1 NAME Lisbon District +2 LANG English +1 NAME Lissabon +2 LANG German +1 NAME District de Lisbonne +2 LANG French +1 NAME Distrikt Lissabon +2 LANG German +1 NAME Lisbonne +2 LANG French +1 TYPE town +2 _GOVTYPE 51 +1 TYPE District +1 _GOV object_1177032 +1 MAP +2 LATI N38.72564 +2 LONG W9.150028 +1 _LOC @GEO2264397@ +2 TYPE POLI +1 SOUR @S0008@ +2 PAGE GeoNames ID: 2267056 +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 17:01:25 +0 @object_190123@ _LOC +1 NAME Denmark +2 LANG English +2 ABBR DK +3 TYPE ISO3166 +1 NAME Dinamarca +2 LANG Spanish +1 NAME Danemark +2 LANG French +1 NAME Dänemark +2 LANG German +1 NAME Danmark +2 LANG Swedish +1 NAME Danmark +2 LANG Danish +1 TYPE kingdom +2 _GOVTYPE 31 +1 _GOV object_190123 +1 MAP +2 LATI N55.482291 +2 LONG E9.986007 +1 _LOC @object_1176980@ +2 DATE AFT 1973 +2 TYPE POLI +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 16:26:21 +0 @object_364181@ _LOC +1 NAME Bornholm +2 LANG Danish +1 TYPE Amt (dänisches) +2 _GOVTYPE 57 +1 TYPE amt +1 _GOV object_364181 +1 MAP +2 LATI N55.131626 +2 LONG E14.913139 +1 _LOC @object_190123@ +2 TYPE POLI +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 16:14:17 0 @S0000@ SOUR 1 TITL Marriage Certificae 1 REPO @R0002@ @@ -1310,6 +1906,13 @@ 1 CHAN 2 DATE 29 OCT 2016 3 TIME 16:20:39 +0 @S0008@ SOUR +1 TITL GeoNames +1 AUTH Marc Wick +1 REPO @R0007@ +1 CHAN +2 DATE 3 JUN 2019 +3 TIME 16:18:34 0 @R0000@ REPO 1 NAME Business that produced the product: Ancestry.com 1 ADDR 360 W 4800 N, Provo, UT 84604 @@ -1369,6 +1972,11 @@ 1 NOTE @N0013@ 0 @R0005@ REPO 0 @R0006@ REPO +0 @R0007@ REPO +1 NAME www.geonames.org +1 WWW http://www.geonames.org/ +1 EMAIL marc@@geonames.org +1 NOTE @N0021@ 0 @N0000@ NOTE Witness name: John Doe 1 CONT Witness comment: This is a simple test. 0 @N0001@ NOTE Witness name: No Name @@ -1423,6 +2031,20 @@ 0 @N0017@ NOTE A citation Note Source text 0 @N0018@ NOTE Another Citation Note 0 @N0019@ NOTE A bad photo for sure +0 @N0020@ NOTE Timeline +1 CONT French Texas 1684–1689 +1 CONT Spanish Texas 1690–1821 +1 CONT Mexican Texas 1821–1836 +1 CONT Republic of Texas 1836–1845 +1 CONT Statehood 1845–1860 +1 CONT Civil War Era 1861–1865 +1 CONT Reconstruction 1865–1899 +1 CONT +0 @N0021@ NOTE GeoNames was founded by Marc Wick. You can reach him at marc@@geona +1 CONC mes.org +1 CONT GeoNames is a project of Unxos GmbH, Weingartenstrasse 8, 8708 Männedorf +1 CONC , Switzerland. +1 CONT This work is licensed under a Creative Commons Attribution 3.0 License 0 @O0000@ OBJE 1 FILE d:\users\prc\documents\gramps\data\tests\O0.jpg 2 FORM jpg @@ -1431,4 +2053,11 @@ 1 CHAN 2 DATE 29 OCT 2016 3 TIME 15:23:37 +0 @O0001@ OBJE +1 FILE d:\users\prc\documents\gramps2\data\tests\1024px-Texas_flag_map.svg.png +2 FORM png +2 TITL 1024px-Texas_flag_map.svg +1 CHAN +2 DATE 10 JUN 2018 +3 TIME 19:14:55 0 TRLR diff --git a/gramps/plugins/export/exportgedcom.py b/gramps/plugins/export/exportgedcom.py index 4361eb565eb..75d7476d4fc 100644 --- a/gramps/plugins/export/exportgedcom.py +++ b/gramps/plugins/export/exportgedcom.py @@ -34,17 +34,17 @@ #------------------------------------------------------------------------- import os import time +import re #------------------------------------------------------------------------- # # Gramps modules # #------------------------------------------------------------------------- -from gramps.gen.const import GRAMPS_LOCALE as glocale -_ = glocale.translation.gettext from gramps.gen.lib import (AttributeType, ChildRefType, Citation, Date, EventRoleType, EventType, LdsOrd, NameType, - PlaceType, NoteType, Person, UrlType) + NoteType, Person, PlaceType, PlaceHierType, + UrlType) from gramps.version import VERSION import gramps.plugins.lib.libgedcom as libgedcom from gramps.gen.errors import DatabaseError @@ -53,8 +53,10 @@ from gramps.gen.updatecallback import UpdateCallback from gramps.gen.utils.file import media_path_full from gramps.gen.utils.place import conv_lat_lon -from gramps.gen.utils.location import get_main_location -from gramps.gen.display.place import displayer as _pd +from gramps.gen.utils.location import get_location_list +from gramps.gen.utils.grampslocale import _LOCALE_NAMES +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext #------------------------------------------------------------------------- # @@ -239,14 +241,16 @@ def write_gedcom_file(self, filename): source_len = self.dbase.get_number_of_sources() repo_len = self.dbase.get_number_of_repositories() note_len = self.dbase.get_number_of_notes() / NOTES_PER_PERSON + place_len = self.dbase.get_number_of_places() total_steps = (person_len + family_len + source_len + repo_len + - note_len) + note_len + place_len) self.set_total(total_steps) self._header(filename) self._submitter() self._individuals() self._families() + self._places() self._sources() self._repos() self._notes() @@ -351,9 +355,9 @@ def _header(self, filename): lang = glocale.language[0] if lang and len(lang) >= 2: - lang_code = LANGUAGES.get(lang[0:2]) + lang_code = _LOCALE_NAMES.get(lang[0:2]) if lang_code: - self._writeln(1, 'LANG', lang_code) + self._writeln(1, 'LANG', lang_code[2].split(' ')[0]) def _submitter(self): """ @@ -1032,6 +1036,173 @@ def _note_record(self, note): self._writeln(0, '@%s@' % note.get_gramps_id(), 'NOTE ' + note.get()) + def _places(self): + """ + Write out the list of places, sorting by Gramps ID. + """ + self.set_text(_("Writing places")) + place_cnt = 0 + sorted_list = sort_handles_by_id(self.dbase.get_place_handles(), + self.dbase.get_place_from_handle) + + for place_handle in [hndl[1] for hndl in sorted_list]: + self.update() + place_cnt += 1 + place = self.dbase.get_place_from_handle(place_handle) + if place is None: + continue + self._place_record(place) + + _gramps_id = re.compile(r' *[^\d]{0,3}(\d+){3,9}[^\d]{0,3}') + + def _place_record(self, place): + """ + This record is all a Gedcom L extension + + 0 @@ _LOC + 1 NAME {1:M} + 2 DATE {0:1} + 2 _NAMC {0:1} # Unused + 2 ABBR {0:M} + 3 TYPE {0:1} + 2 LANG {0:1} + 2 << SOURCE_CITATION >> {0:M} + 1 TYPE {0:M} + 2 _GOVTYPE {0:1} + 2 DATE {0:1} + 2 << SOURCE_CITATION >> {0:M} + 1 _FPOST {0:M} # deprecated + 2 DATE {0:1} # Unused + 1 _POST {0:M} + 2 DATE {0:1} # Unused + 2 << SOURCE_CITATION >> {0:M} + 1 _GOV {0:1} + 1 _FSTAE {0:1} # deprecated + 1 _FCTRY {0:1} # deprecated + 1 MAP {0:1} + 1 _MAIDENHEAD {0:1} + 1 EVEN [ | ] {0:M} # Unused + 2 << EVENT_DETAIL >> {0:1} # Unused + 1 _LOC @@ {0:M} + 2 TYPE {1:1} + 2 DATE {0:1} + 2 << SOURCE_CITATION >> {0:M} # Unused + 1 _DMGD {0:M} + 2 DATE {0:1} # Unused + 2 << SOURCE_CITATION >> {0:M} + 2 TYPE {1:1} # Unused + 1 _AIDN {0:M} + 2 DATE {0:1} # Unused + 2 << SOURCE_CITATION >> {0:M} + 2 TYPE {1:1} # Unused + 1 << MULTIMEDIA_LINK >> {0:M} + 1 << NOTE_STRUCTURE >> {0:M} + 1 << SOURCE_CITATION >> {0:M} + 1 << CHANGE_DATE >> {0:1} + + """ + self._writeln(0, '@%s@' % place.get_gramps_id(), '_LOC') + # write the names + for name in place.get_names(): + self._writeln(1, "NAME", name.value) + self._date(2, name.get_date_object()) + if name.lang: + lang_code = _LOCALE_NAMES.get(name.lang) + if lang_code: + self._writeln(2, 'LANG', lang_code[2].split(' ')[0]) + self._source_references(name.get_citation_list(), 2) + for abbr in name.get_abbrevs(): + self._writeln(2, 'ABBR', abbr.value) + self._writeln(3, 'TYPE', abbr.type.xml_str()) + # write the place types. + for ptype in place.get_types(): + if ptype == PlaceType.UNKNOWN: + continue + self._writeln(1, "TYPE", ptype.name) + if ptype.pt_id.startswith("GOV_"): + self._writeln(2, "_GOVTYPE", ptype.pt_id[4:]) + self._date(2, ptype.get_date_object()) + self._source_references(ptype.get_citation_list(), 2) + # Write out attributes that match GEDCOM L items + for attr in place.get_attribute_list(): + if attr.type == AttributeType.AIDN: + atype = '_AIDN' + elif attr.type == AttributeType.DMGD: + atype = '_DMGD' + elif attr.type == AttributeType.MAIDEN: + atype = '_MAIDENHEAD' + elif attr.type == AttributeType.POSTAL: + atype = '_POST' + else: + continue + # Find the Type: and Date: values, if any, assumes Type always + # precedes Date + txt = attr.get_value() + ftype = txt.find('; ' + _("Type:")) + fdate = txt.find('; ' + _("Date:")) + if ftype == -1 and fdate == -1: + self._writeln(1, atype, txt) + elif fdate == -1: + self._writeln(1, atype, txt[: ftype]) + self._writeln(2, "TYPE", txt[ftype + len(_("Type:")) + 3 :]) + elif ftype == -1: + self._writeln(1, atype, txt[: fdate]) + self._writeln(2, "DATE", txt[fdate + len(_("Date:")) + 3 :]) + else: + self._writeln(1, atype, txt[: ftype]) + self._writeln(2, "TYPE", + txt[ftype + len(_("Type:")) + 3 : fdate]) + self._writeln(2, "DATE", txt[fdate + len(_("Date:")) + 3 :]) + self._source_references(attr.get_citation_list(), 2) + + # See if this is a normal Gramps ID (if not, GOV ID) + if not (self._gramps_id.match(place.gramps_id) or + place.gramps_id.startswith("GEO")): + self._writeln(1, "_GOV", place.gramps_id) + # write LAT/LON + longitude = place.get_longitude() + latitude = place.get_latitude() + if longitude and latitude: + (latitude, longitude) = conv_lat_lon(latitude, longitude, "GEDCOM") + if longitude and latitude: + self._writeln(1, "MAP") + self._writeln(2, 'LATI', latitude) + self._writeln(2, 'LONG', longitude) + # write place references + h_type = {PlaceHierType.ADMIN : 'POLI', + PlaceHierType.RELI : 'RELI', + PlaceHierType.GEOG : 'GEOG', + PlaceHierType.CULT : 'CULT'} + for pref in place.placeref_list: + ref_place = self.dbase.get_place_from_handle(pref.ref) + if ref_place: # in case we ever filter places + self._writeln(1, "_LOC", "@%s@" % ref_place.gramps_id) + self._date(2, pref.get_date_object()) + # types? + htype = h_type.get(int(pref.type)) + if htype: + self._writeln(2, "TYPE", htype) + self._source_references(pref.get_citation_list(), 2) + # deal with place events + for event_ref in place.get_event_ref_list(): + event = self.dbase.get_event_from_handle(event_ref.ref) + if event is None: + continue + descr = event.get_description() + if descr: + self._writeln(1, 'EVEN', descr) + else: + self._writeln(1, 'EVEN') + the_type = event.get_type().xml_str() + if the_type: + self._writeln(2, 'TYPE', the_type) + self._dump_event_stats(event, event_ref) + # write the standard stuff + self._photos(place.get_media_list(), 1) + self._note_references(place.get_note_list(), 1) + self._source_references(place.get_citation_list(), 1) + self._change(place.get_change_time(), 1) + def _repos(self): """ Write out the list of repositories, sorting by Gramps ID. @@ -1485,11 +1656,36 @@ def _place(self, place, dateobj, level): +2 LATI {1:1} +2 LONG {1:1} +1 <> {0:M} + +1 _LOC @@ {0:1} # Gedcom L extension + +1 _GOV {0:1} # Gedcom L extension + + The Gedcom standard shows that an optional address structure can + be written out in the event detail. + http://homepages.rootsweb.com/~pmcbride/gedcom/55gcch2.htm#EVENT_DETAIL + We use this when the place type is 'Address', as that is how the Gedcom + importer identified ADDR records to start with. If the Address record + is enclosed, we finish it off with standard PLAC record. + + Any other types of places are output with the comma delimited list of + enclosing places and the PLAC.FORM record to idetify their types. + The _LOC Gedcom extension is also added to reference later _LOC + records. """ if place is None: return - place_name = _pd.display(self.dbase, place, dateobj) + loc_list = get_location_list(self.dbase, place, date=dateobj, + lang='en') + if str(loc_list[0][1]) == _("Address"): # PlaceType + self._writeln(level, "ADDR", loc_list[0][0]) + del loc_list[0] + if not loc_list: + return + place_name = ", ".join(item[0] for item in loc_list) + place_form = ", ".join(item[1].name for item in loc_list) + self._writeln(level, "PLAC", place_name.replace('\r', ' '), limit=120) + if len(loc_list) != 1 or loc_list[0][1] != PlaceType.UNKNOWN: + self._writeln(level + 1, "FORM", place_form, limit=120) longitude = place.get_longitude() latitude = place.get_latitude() if longitude and latitude: @@ -1498,33 +1694,12 @@ def _place(self, place, dateobj, level): self._writeln(level + 1, "MAP") self._writeln(level + 2, 'LATI', latitude) self._writeln(level + 2, 'LONG', longitude) - - # The Gedcom standard shows that an optional address structure can - # be written out in the event detail. - # http://homepages.rootsweb.com/~pmcbride/gedcom/55gcch2.htm#EVENT_DETAIL - location = get_main_location(self.dbase, place) - street = location.get(PlaceType.STREET) - locality = location.get(PlaceType.LOCALITY) - city = location.get(PlaceType.CITY) - state = location.get(PlaceType.STATE) - country = location.get(PlaceType.COUNTRY) - postal_code = place.get_code() - - if street or locality or city or state or postal_code or country: - self._writeln(level, "ADDR", street) - if street: - self._writeln(level + 1, 'ADR1', street) - if locality: - self._writeln(level + 1, 'ADR2', locality) - if city: - self._writeln(level + 1, 'CITY', city) - if state: - self._writeln(level + 1, 'STAE', state) - if postal_code: - self._writeln(level + 1, 'POST', postal_code) - if country: - self._writeln(level + 1, 'CTRY', country) - + # cross ref to Gedcom L _LOC record + self._writeln(level + 1, "_LOC", "@%s@" % place.gramps_id) + # See if this is a normal Gramps ID (if not, GOV ID) + if not (self._gramps_id.match(place.gramps_id) or + place.gramps_id.startswith("GEO")): + self._writeln(level + 1, "_GOV", place.gramps_id) self._note_references(place.get_note_list(), level + 1) def __write_addr(self, level, addr): From 7cbaf76f056321680a679eb89aaae829ba50e18f Mon Sep 17 00:00:00 2001 From: prculley Date: Sun, 10 Nov 2019 15:01:33 -0600 Subject: [PATCH 06/26] Make Gedcom L enhanced places an option for GEDCOM export --- gramps/plugins/export/export.gpr.py | 2 +- gramps/plugins/export/exportgedcom.py | 57 +++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/gramps/plugins/export/export.gpr.py b/gramps/plugins/export/export.gpr.py index 4b18819621b..8c75a0bdc64 100644 --- a/gramps/plugins/export/export.gpr.py +++ b/gramps/plugins/export/export.gpr.py @@ -83,7 +83,7 @@ plg.fname = 'exportgedcom.py' plg.ptype = EXPORT plg.export_function = 'export_data' -plg.export_options = 'WriterOptionBox' +plg.export_options = 'GedcomWriterOptionBox' plg.export_options_title = _('GEDCOM export options') plg.extension = "ged" diff --git a/gramps/plugins/export/exportgedcom.py b/gramps/plugins/export/exportgedcom.py index 75d7476d4fc..18d202861a8 100644 --- a/gramps/plugins/export/exportgedcom.py +++ b/gramps/plugins/export/exportgedcom.py @@ -199,6 +199,40 @@ def event_has_subordinate_data(event, event_ref): return False +#------------------------------------------------------------------------- +# +# GedcomWriter Options +# +#------------------------------------------------------------------------- +class GedcomWriterOptionBox(WriterOptionBox): + """ + Create a VBox with the option widgets and define methods to retrieve + the options. + + """ + def __init__(self, person, dbstate, uistate, track=[], window=None): + WriterOptionBox.__init__(self, person, dbstate, uistate, track=track, + window=window) + self.include_ext_places = 0 + self.include_ext_places_chk = None + + def get_option_box(self): + from gi.repository import Gtk + option_box = WriterOptionBox.get_option_box(self) + self.include_ext_places_chk = Gtk.CheckButton( + label=_("Include extended place information as defined by " + "GEDCOM L group")) + self.include_ext_places_chk.set_active(1) + option_box.pack_start(self.include_ext_places_chk, False, True, 0) + + return option_box + + def parse_options(self): + WriterOptionBox.parse_options(self) + if self.include_ext_places_chk: + self.include_ext_places = self.include_ext_places_chk.get_active() + + #------------------------------------------------------------------------- # # GedcomWriter class @@ -217,6 +251,7 @@ def __init__(self, database, user, option_box=None): self.dirname = None self.gedcom_file = None self.progress_cnt = 0 + self.include_ext_places = 1 self.setup(option_box) def setup(self, option_box): @@ -228,6 +263,7 @@ def setup(self, option_box): if option_box: option_box.parse_options() self.dbase = option_box.get_filtered_database(self.dbase, self) + self.include_ext_places = option_box.include_ext_places def write_gedcom_file(self, filename): """ @@ -241,7 +277,10 @@ def write_gedcom_file(self, filename): source_len = self.dbase.get_number_of_sources() repo_len = self.dbase.get_number_of_repositories() note_len = self.dbase.get_number_of_notes() / NOTES_PER_PERSON - place_len = self.dbase.get_number_of_places() + if self.include_ext_places: + place_len = self.dbase.get_number_of_places() + else: + place_len = 0 total_steps = (person_len + family_len + source_len + repo_len + note_len + place_len) @@ -250,7 +289,8 @@ def write_gedcom_file(self, filename): self._submitter() self._individuals() self._families() - self._places() + if self.include_ext_places: + self._places() self._sources() self._repos() self._notes() @@ -1694,12 +1734,13 @@ def _place(self, place, dateobj, level): self._writeln(level + 1, "MAP") self._writeln(level + 2, 'LATI', latitude) self._writeln(level + 2, 'LONG', longitude) - # cross ref to Gedcom L _LOC record - self._writeln(level + 1, "_LOC", "@%s@" % place.gramps_id) - # See if this is a normal Gramps ID (if not, GOV ID) - if not (self._gramps_id.match(place.gramps_id) or - place.gramps_id.startswith("GEO")): - self._writeln(level + 1, "_GOV", place.gramps_id) + if self.include_ext_places: + # cross ref to Gedcom L _LOC record + self._writeln(level + 1, "_LOC", "@%s@" % place.gramps_id) + # See if this is a normal Gramps ID (if not, GOV ID) + if not (self._gramps_id.match(place.gramps_id) or + place.gramps_id.startswith("GEO")): + self._writeln(level + 1, "_GOV", place.gramps_id) self._note_references(place.get_note_list(), level + 1) def __write_addr(self, level, addr): From 8590fe35b5b181b04462f90972bc83225eca2846 Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 09:52:13 -0500 Subject: [PATCH 07/26] GEPS045 tests --- gramps/gen/lib/test/merge_test.py | 153 ++++++++++++++++-------- gramps/gen/merge/test/merge_ref_test.py | 41 ++++--- 2 files changed, 129 insertions(+), 65 deletions(-) diff --git a/gramps/gen/lib/test/merge_test.py b/gramps/gen/lib/test/merge_test.py index 69997e36dd2..49dd9e6b82c 100644 --- a/gramps/gen/lib/test/merge_test.py +++ b/gramps/gen/lib/test/merge_test.py @@ -41,6 +41,8 @@ from ..surnamebase import SurnameBase from ..tagbase import TagBase from ..const import IDENTICAL, EQUAL, DIFFERENT +from gramps.gen.datehandler import parser as _dp + class PrivacyBaseTest: def test_privacy_merge(self): @@ -1408,115 +1410,166 @@ def setUp(self): def test_merge_primary_identical(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) + self.phoenix.set_type('City') self.titanic.set_title('Place 2') self.titanic.set_name(self.amsterdam) - self.titanic.set_type(PlaceType.CITY) + self.titanic.set_type('City') self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) + self.ref_obj.set_type('City') self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_primary_different(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) + self.phoenix.set_type('City') self.titanic.set_title('Place 2') self.titanic.set_name(self.rotterdam) - self.titanic.set_type(PlaceType.CITY) + self.titanic.set_type('City') self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_both_different(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.utrecht) + self.phoenix.set_type('City') + self.phoenix.add_name(self.utrecht) self.titanic.set_title('Place 2') self.titanic.set_name(self.rotterdam) - self.titanic.set_type(PlaceType.CITY) - self.titanic.add_alternative_name(self.leiden) + self.titanic.set_type('City') + self.titanic.add_name(self.leiden) self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - # Base name shouldn't be in alt_names list - # self.ref_obj.add_alternative_name(self.amsterdam) - # alt_names must be in correct order for test to pass - self.ref_obj.add_alternative_name(self.utrecht) - self.ref_obj.add_alternative_name(self.rotterdam) - self.ref_obj.add_alternative_name(self.leiden) + self.ref_obj.set_type('City') + # names must be in correct order for test to pass + self.ref_obj.add_name(self.utrecht) + self.ref_obj.add_name(self.rotterdam) + self.ref_obj.add_name(self.leiden) + self.phoenix.merge(self.titanic) + self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) + + def test_merge_both_different_sort_date(self): + self.amsterdam.date = _dp.parse("before 1960") + self.phoenix.set_name(self.amsterdam) + self.utrecht.date = _dp.parse("from 1960 to 1970") + self.phoenix.add_name(self.utrecht) + ptype1 = PlaceType('Hamlet', + date=_dp.parse("before 1960")) + self.phoenix.set_type(ptype1) + ptype2 = PlaceType('Village', + date=_dp.parse("from 1960 to 1970")) + self.phoenix.add_type(ptype2) + self.titanic.set_title('Place 2') + self.rotterdam.date = _dp.parse("after 1980") + self.titanic.set_name(self.rotterdam) + self.leiden.date = _dp.parse("from 1970 to 1980") + self.titanic.add_name(self.leiden) + ptype3 = PlaceType('City', + date=_dp.parse("after 1980")) + self.titanic.set_type(ptype3) + ptype4 = PlaceType('Town', + date=_dp.parse("from 1970 to 1980")) + self.titanic.add_type(ptype4) + # names must be in correct order for test to pass + self.ref_obj.set_name(self.rotterdam) + self.ref_obj.add_name(self.utrecht) + self.ref_obj.add_name(self.amsterdam) + self.ref_obj.add_name(self.leiden) + self.ref_obj.add_type(ptype3) + self.ref_obj.add_type(ptype2) + self.ref_obj.add_type(ptype1) + self.ref_obj.add_type(ptype4) + self.phoenix.merge(self.titanic) + self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) + + def test_merge_both_different_sort_lang(self): + self.amsterdam.lang = 'de' + self.phoenix.set_name(self.amsterdam) + self.utrecht.lang = 'fi' + self.phoenix.add_name(self.utrecht) + self.phoenix.set_type('City') + self.titanic.set_title('Place 2') + self.titanic.set_name(self.rotterdam) + self.titanic.add_name(self.leiden) + self.titanic.set_type('City') + # names must be in correct order for test to pass + self.ref_obj.add_name(self.rotterdam) + self.ref_obj.add_name(self.amsterdam) + self.ref_obj.add_name(self.utrecht) + self.ref_obj.add_name(self.leiden) + self.ref_obj.set_type('City') self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_alternative_identical(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.rotterdam) + self.phoenix.set_type('City') + self.phoenix.add_name(self.rotterdam) self.titanic.set_title('Place 2') self.titanic.set_name(self.amsterdam) - self.titanic.set_type(PlaceType.CITY) - self.titanic.add_alternative_name(self.rotterdam) + self.titanic.set_type('City') + self.titanic.add_name(self.rotterdam) self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_alternative_different(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.rotterdam) + self.phoenix.set_type('City') + self.phoenix.add_name(self.rotterdam) self.titanic.set_title('Place 2') self.titanic.set_name(self.amsterdam) - self.titanic.set_type(PlaceType.CITY) - self.titanic.add_alternative_name(self.utrecht) + self.titanic.set_type('City') + self.titanic.add_name(self.utrecht) self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) - self.ref_obj.add_alternative_name(self.utrecht) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) + self.ref_obj.add_name(self.utrecht) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_prialt_identical(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.rotterdam) + self.phoenix.set_type('City') + self.phoenix.add_name(self.rotterdam) self.titanic.set_title('Place 2') self.titanic.set_name(self.rotterdam) - self.titanic.set_type(PlaceType.CITY) + self.titanic.set_type('City') self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_prialt2(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.rotterdam) + self.phoenix.set_type('City') + self.phoenix.add_name(self.rotterdam) self.titanic.set_title('Place 2') self.titanic.set_name(self.rotterdam) - self.titanic.set_type(PlaceType.CITY) - self.titanic.add_alternative_name(self.amsterdam) + self.titanic.set_type('City') + self.titanic.add_name(self.amsterdam) self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) def test_merge_empty(self): self.phoenix.set_name(self.amsterdam) - self.phoenix.set_type(PlaceType.CITY) - self.phoenix.add_alternative_name(self.rotterdam) + self.phoenix.set_type('City') + self.phoenix.add_name(self.rotterdam) self.titanic.set_title('Place 2') # titanic gets empty name - self.titanic.set_type(PlaceType.CITY) - self.titanic.add_alternative_name(self.utrecht) - self.titanic.add_alternative_name(PlaceName()) # empty alt_name + self.titanic.set_type('City') + self.titanic.add_name(self.utrecht) + self.titanic.add_name(PlaceName()) # empty alt_name self.ref_obj.set_name(self.amsterdam) - self.ref_obj.set_type(PlaceType.CITY) - self.ref_obj.add_alternative_name(self.rotterdam) - self.ref_obj.add_alternative_name(self.utrecht) + self.ref_obj.set_type('City') + self.ref_obj.add_name(self.rotterdam) + self.ref_obj.add_name(self.utrecht) self.phoenix.merge(self.titanic) self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) diff --git a/gramps/gen/merge/test/merge_ref_test.py b/gramps/gen/merge/test/merge_ref_test.py index 265c91a1a49..dcdd3c47672 100644 --- a/gramps/gen/merge/test/merge_ref_test.py +++ b/gramps/gen/merge/test/merge_ref_test.py @@ -35,7 +35,7 @@ from gramps.gen.user import User from gramps.gen.const import DATA_DIR, USER_PLUGINS, TEMP_DIR from gramps.version import VERSION -from gramps.gen.lib import Name, Surname +from gramps.gen.lib import Name, Surname, PlaceType from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -144,9 +144,9 @@ def check_results(self, input_doc, expect_doc, result_str, err_str, inpt = open(input_file, mode='wb') inpt.write(inp) inpt.close() - result = result.decode('utf-8') - expect = expect.decode('utf-8') - diff = difflib.ndiff(result, expect) + result = open(result_file, 'r', encoding='utf-8') + expect = open(expect_file, 'r', encoding='utf-8') + diff = difflib.ndiff(result.readlines(), expect.readlines()) msg = "" for line in diff: msg += line @@ -261,13 +261,15 @@ def setUp(self): - + Place 0 + - + Place 1 + @@ -313,7 +315,8 @@ def test_place_merge(self): placeobj.getparent().remove(placeobj) placeobj = expect.xpath("//g:placeobj[@handle='_p0000']", namespaces={"g": NS_G})[0] - ET.SubElement(placeobj, NSP + 'pname', value='Place 1') + pname = ET.Element(NSP + 'pname', value='Place 1') + placeobj.insert(2, pname) self.do_case('P0000', 'P0001', self.basedoc, expect) def test_citation_merge(self): @@ -414,13 +417,15 @@ def setUp(self): - + Place 0 + - + Place 1 + @@ -465,7 +470,8 @@ def test_place_merge(self): placeobj.getparent().remove(placeobj) placeobj = expect.xpath("//g:placeobj[@handle='_p0000']", namespaces={"g": NS_G})[0] - ET.SubElement(placeobj, NSP + 'pname', value='Place 1') + pname = ET.Element(NSP + 'pname', value='Place 1') + placeobj.insert(2, pname) self.do_case('P0000', 'P0001', self.basedoc, expect) def test_citation_merge(self): @@ -552,13 +558,15 @@ def setUp(self): - + Place 0 + - + Place 1 + @@ -592,7 +600,8 @@ def test_place_merge(self): placeobj.getparent().remove(placeobj) placeobj = expect.xpath("//g:placeobj[@handle='_p0000']", namespaces={"g": NS_G})[0] - ET.SubElement(placeobj, NSP + 'pname', value='Place 1') + pname = ET.Element(NSP + 'pname', value='Place 1') + placeobj.insert(2, pname) self.do_case('P0000', 'P0001', self.basedoc, expect) def test_citation_merge(self): @@ -661,16 +670,18 @@ def setUp(self): - + Place 0 + - + Place 1 + From c66673c213abb3367e1e6834b44615835950c2ca Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 09:55:05 -0500 Subject: [PATCH 08/26] GEPS045 filters and rules --- gramps/gen/filters/rules/place/__init__.py | 5 +- .../gen/filters/rules/place/_hasattribute.py | 49 ++++++++ gramps/gen/filters/rules/place/_hasdata.py | 22 ++-- gramps/gen/filters/rules/place/_hasevent.py | 90 +++++++++++++++ gramps/gen/filters/rules/place/_hasplace.py | 105 ------------------ .../filters/rules/test/place_rules_test.py | 2 +- gramps/gen/proxy/filter.py | 16 ++- gramps/gen/proxy/private.py | 67 +++++++---- gramps/gen/proxy/proxybase.py | 21 ++++ gramps/gen/proxy/referencedbyselection.py | 14 +++ gramps/gui/editors/filtereditor.py | 51 +++++++-- .../filters/sidebar/_placesidebarfilter.py | 39 +++---- 12 files changed, 311 insertions(+), 170 deletions(-) create mode 100644 gramps/gen/filters/rules/place/_hasattribute.py create mode 100644 gramps/gen/filters/rules/place/_hasevent.py delete mode 100644 gramps/gen/filters/rules/place/_hasplace.py diff --git a/gramps/gen/filters/rules/place/__init__.py b/gramps/gen/filters/rules/place/__init__.py index 793cf3ad8ac..4df75f2e00d 100644 --- a/gramps/gen/filters/rules/place/__init__.py +++ b/gramps/gen/filters/rules/place/__init__.py @@ -25,7 +25,9 @@ """ from ._allplaces import AllPlaces +from ._hasattribute import HasAttribute from ._hascitation import HasCitation +from ._hasevent import HasEvent from ._hasgallery import HasGallery from ._hasidof import HasIdOf from ._regexpidof import RegExpIdOf @@ -37,7 +39,6 @@ from ._hassourceof import HasSourceOf from ._placeprivate import PlacePrivate from ._matchesfilter import MatchesFilter -from ._hasplace import HasPlace from ._hasdata import HasData from ._hasnolatorlon import HasNoLatOrLon from ._inlatlonneighborhood import InLatLonNeighborhood @@ -51,7 +52,9 @@ editor_rule_list = [ AllPlaces, + HasAttribute, HasCitation, + HasEvent, HasGallery, HasIdOf, RegExpIdOf, diff --git a/gramps/gen/filters/rules/place/_hasattribute.py b/gramps/gen/filters/rules/place/_hasattribute.py new file mode 100644 index 00000000000..ca30620ee35 --- /dev/null +++ b/gramps/gen/filters/rules/place/_hasattribute.py @@ -0,0 +1,49 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Gary Burton +# Copyright (C) 2019 Matthias Kemmer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# ------------------------------------------------------------------------- +# +# Standard Python modules +# +# ------------------------------------------------------------------------- +from ....const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from .._hasattributebase import HasAttributeBase + + +# ------------------------------------------------------------------------- +# +# HasAttribute +# +# ------------------------------------------------------------------------- +class HasAttribute(HasAttributeBase): + """Rule that checks for a place with a particular attribute""" + + labels = [_('Place attribute:'), _('Value:')] + name = _('Places with the attribute ') + description = _("Matches places with the attribute " + "of a particular value") diff --git a/gramps/gen/filters/rules/place/_hasdata.py b/gramps/gen/filters/rules/place/_hasdata.py index 63d323a3b30..ecc45ef9e4f 100644 --- a/gramps/gen/filters/rules/place/_hasdata.py +++ b/gramps/gen/filters/rules/place/_hasdata.py @@ -35,6 +35,7 @@ from .. import Rule from ....lib import PlaceType + #------------------------------------------------------------------------- # # HasData @@ -45,10 +46,8 @@ class HasData(Rule): Rule that checks for a place with a particular value """ - labels = [ _('Name:'), - _('Place type:'), - _('Code:'), - ] + labels = [_('Name:'), + _('Place type:')] name = _('Places matching parameters') description = _('Matches places with particular parameters') category = _('General filters') @@ -58,17 +57,16 @@ def prepare(self, db, user): self.place_type = self.list[1] if self.place_type: - self.place_type = PlaceType() - self.place_type.set_from_xml_str(self.list[1]) + self.place_type = PlaceType(self.list[1]) - def apply(self, db, place): + def apply(self, _db, place): if not self.match_name(place): return False - if self.place_type and place.get_type() != self.place_type: - return False - - if not self.match_substring(2, place.get_code()): + if self.place_type: + for typ in place.get_types(): # place types list + if typ.is_same(self.place_type): + return True return False return True @@ -77,7 +75,7 @@ def match_name(self, place): """ Match any name in a list of names. """ - for name in place.get_all_names(): + for name in place.get_names(): if self.match_substring(0, name.get_value()): return True return False diff --git a/gramps/gen/filters/rules/place/_hasevent.py b/gramps/gen/filters/rules/place/_hasevent.py new file mode 100644 index 00000000000..4389e02710c --- /dev/null +++ b/gramps/gen/filters/rules/place/_hasevent.py @@ -0,0 +1,90 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2002-2006 Donald N. Allingham +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from ....datehandler import parser +from ....display.place import displayer as place_displayer +from ....lib.eventtype import EventType +from .. import Rule +from ....const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext + + +#------------------------------------------------------------------------- +# +# HasFamilyEvent +# +#------------------------------------------------------------------------- +class HasEvent(Rule): + """Rule that checks for a place which has an event + with a particular value""" + + labels = [_('Place event:'), + _('Date:'), + _('Place:'), + _('Description:')] + name = _('Places with the ') + description = _("Matches places with an event of a particular value") + category = _('Event filters') + allow_regex = True + + def prepare(self, db, user): + self.date = None + try: + if self.list[1]: + self.date = parser.parse(self.list[1]) + except: + pass + + def apply(self, db, place): + for event_ref in place.get_event_ref_list(): + if not event_ref: + continue + event_handle = event_ref.ref + event = db.get_event_from_handle(event_handle) + val = True + if self.list[0]: + specified_type = EventType() + specified_type.set_from_xml_str(self.list[0]) + if event.type != specified_type: + val = False + if self.list[3]: + if not self.match_substring(3, event.get_description()): + val = False + if self.date: + if not event.get_date_object().match(self.date): + val = False + if self.list[2]: + place_id = event.get_place_handle() + if place_id: + place = db.get_place_from_handle(place_id) + place_title = place_displayer.display(db, place) + if not self.match_substring(2, place_title): + val = False + else: + val = False + + if val: + return True + return False diff --git a/gramps/gen/filters/rules/place/_hasplace.py b/gramps/gen/filters/rules/place/_hasplace.py deleted file mode 100644 index 9510bd940af..00000000000 --- a/gramps/gen/filters/rules/place/_hasplace.py +++ /dev/null @@ -1,105 +0,0 @@ -# -# Gramps - a GTK+/GNOME based genealogy program -# -# Copyright (C) 2002-2006 Donald N. Allingham -# Copyright (C) 2008 Gary Burton -# Copyright (C) 2010 Nick Hall -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# - -#------------------------------------------------------------------------- -# -# Standard Python modules -# -#------------------------------------------------------------------------- -from ....const import GRAMPS_LOCALE as glocale -_ = glocale.translation.sgettext - -#------------------------------------------------------------------------- -# -# Gramps modules -# -#------------------------------------------------------------------------- -from .. import Rule -from ....lib import PlaceType -from ....utils.location import get_locations - -#------------------------------------------------------------------------- -# -# HasPlace -# -#------------------------------------------------------------------------- -class HasPlace(Rule): - """Rule that checks for a place with a particular value""" - - labels = [ _('Title:'), - _('Street:'), - _('Locality:'), - _('City:'), - _('County:'), - _('State:'), - _('Country:'), - _('ZIP/Postal Code:'), - _('Church Parish:'), - ] - name = _('Places matching parameters') - description = _("Matches places with particular parameters") - category = _('General filters') - allow_regex = True - - TYPE2FIELD = {PlaceType.STREET: 1, - PlaceType.LOCALITY: 2, - PlaceType.CITY: 3, - PlaceType.COUNTY: 4, - PlaceType.STATE: 5, - PlaceType.COUNTRY: 6, - PlaceType.PARISH: 8} - - def apply(self, db, place): - if not self.match_substring(0, place.get_title()): - return False - - if not self.match_substring(7, place.get_code()): - return False - - # If no location data was given then we're done: match - if not any(self.list[1:7] + [self.list[8]]): - return True - - for location in get_locations(db, place): - if self.check(location): - return True - - return False - - def check(self, location): - """ - Check each location for a match. - """ - for place_type, field in self.TYPE2FIELD.items(): - name_list = location.get(place_type, ['']) - if not self.match_name(field, name_list): - return False - return True - - def match_name(self, field, name_list): - """ - Match any name in a list of names. - """ - for name in name_list: - if self.match_substring(field, name): - return True - return False diff --git a/gramps/gen/filters/rules/test/place_rules_test.py b/gramps/gen/filters/rules/test/place_rules_test.py index c850d5ecf1b..0fd8dce06c1 100644 --- a/gramps/gen/filters/rules/test/place_rules_test.py +++ b/gramps/gen/filters/rules/test/place_rules_test.py @@ -167,7 +167,7 @@ def test_hasdata(self): """ Test HasData rule. """ - rule = HasData(['Albany', 'County', '']) + rule = HasData(['Albany', 'County']) self.assertEqual(self.filter_with_rule(rule), set(['c9658726d602acadb74e330116a'])) diff --git a/gramps/gen/proxy/filter.py b/gramps/gen/proxy/filter.py index 2b10a71a83b..4227c4b0ccf 100644 --- a/gramps/gen/proxy/filter.py +++ b/gramps/gen/proxy/filter.py @@ -186,7 +186,7 @@ def get_place_from_handle(self, handle): place = self.db.get_place_from_handle(handle) if place: - # Filter notes out + # Filter notes and events out self.sanitize_notebase(place) media_ref_list = place.get_media_list() @@ -195,6 +195,20 @@ def get_place_from_handle(self, handle): attributes = media_ref.get_attribute_list() for attribute in attributes: self.sanitize_notebase(attribute) + attributes = place.get_attribute_list() + for attribute in attributes: + self.sanitize_notebase(attribute) + eref_list = place.get_event_ref_list() + + new_eref_list = [ref for ref in eref_list + if ref.ref in self.elist] + for event_ref in new_eref_list: + self.sanitize_notebase(event_ref) + attributes = event_ref.get_attribute_list() + for attribute in attributes: + self.sanitize_notebase(attribute) + + place.set_event_ref_list(new_eref_list) return place diff --git a/gramps/gen/proxy/private.py b/gramps/gen/proxy/private.py index 051e01f2e68..d53bdc09844 100644 --- a/gramps/gen/proxy/private.py +++ b/gramps/gen/proxy/private.py @@ -40,7 +40,8 @@ # #------------------------------------------------------------------------- from ..lib import (MediaRef, Attribute, Address, EventRef, - Person, Name, Source, RepoRef, Media, Place, Event, + Person, Name, Source, RepoRef, Media, + Place, PlaceName, PlaceRef, PlaceType, Event, Family, ChildRef, Repository, LdsOrd, Surname, Citation, SrcAttribute, Note, Tag) from .proxybase import ProxyDbBase @@ -489,6 +490,27 @@ def copy_citation_ref_list(db, original_obj, clean_obj): if source and not source.get_privacy(): clean_obj.add_citation(citation_handle) + +def copy_event_ref_list(db, original_obj, clean_obj): + """ + Copies event references from one object to another - excluding references + to private events + + :param db: Gramps database to which the references belongs + :type db: DbBase + :param original_obj: Object that may have private references + :type original_obj: EventBase + :param clean_obj: Object that will have only non-private references + :type original_obj: EventBase + :returns: Nothing + """ + for event_ref in original_obj.get_event_ref_list(): + if event_ref and not event_ref.get_privacy(): + event = db.get_event_from_handle(event_ref.ref) + if event and not event.get_privacy(): + clean_obj.add_event_ref(sanitize_event_ref(db, event_ref)) + + def copy_notes(db, original_obj, clean_obj): """ Copies notes from one object to another - excluding references to private @@ -859,12 +881,7 @@ def sanitize_person(db, person): if name and not name.get_privacy(): new_person.add_alternate_name(sanitize_name(db, name)) - # copy event list - for event_ref in person.get_event_ref_list(): - if event_ref and not event_ref.get_privacy(): - event = db.get_event_from_handle(event_ref.ref) - if event and not event.get_privacy(): - new_person.add_event_ref(sanitize_event_ref(db, event_ref)) + copy_event_ref_list(db, person, new_person) # Copy birth and death after event list to maintain the order. # copy birth event @@ -985,20 +1002,36 @@ def sanitize_place(db, place): new_place.set_latitude(place.get_latitude()) new_place.set_group(place.get_group()) new_place.set_alternate_locations(place.get_alternate_locations()) - new_place.set_name(place.get_name()) - new_place.set_alternative_names(place.get_alternative_names()) - new_place.set_type(place.get_type()) - new_place.set_code(place.get_code()) - new_place.set_placeref_list(place.get_placeref_list()) new_place.set_tag_list(place.get_tag_list()) - + # Copy name list + for name in place.get_names(): + n_name = PlaceName(name) + n_name.citation_list = [] + copy_citation_ref_list(db, name, n_name) + new_place.name_list.append(n_name) + # Copy type list + for ptype in place.get_types(): + n_type = PlaceType(ptype) + n_type.citation_list = [] + copy_citation_ref_list(db, ptype, n_type) + new_place.type_list.append(n_type) + # Copy placeref list + for pref in place.placeref_list: + n_pref = PlaceRef(pref) + n_pref.citation_list = [] + copy_citation_ref_list(db, pref, n_pref) + new_place.placeref_list.append(n_pref) + + copy_event_ref_list(db, place, new_place) copy_citation_ref_list(db, place, new_place) copy_notes(db, place, new_place) copy_media_ref_list(db, place, new_place) copy_urls(db, place, new_place) + copy_attributes(db, place, new_place) return new_place + def sanitize_event(db, event): """ Create a new Event instance based off the passed Event @@ -1089,13 +1122,7 @@ def sanitize_family(db, family): copy_citation_ref_list(db, child_ref, new_ref) new_family.add_child_ref(new_ref) - # Copy event ref list. - for event_ref in family.get_event_ref_list(): - if event_ref and not event_ref.get_privacy(): - event = db.get_event_from_handle(event_ref.ref) - if event and not event.get_privacy(): - new_family.add_event_ref(sanitize_event_ref(db, event_ref)) - + copy_event_ref_list(db, family, new_family) copy_citation_ref_list(db, family, new_family) copy_notes(db, family, new_family) copy_media_ref_list(db, family, new_family) diff --git a/gramps/gen/proxy/proxybase.py b/gramps/gen/proxy/proxybase.py index a841d9930a8..e53e01f66dc 100644 --- a/gramps/gen/proxy/proxybase.py +++ b/gramps/gen/proxy/proxybase.py @@ -797,6 +797,27 @@ def get_origin_types(self): """ return self.db.get_origin_types() + def get_placehier_types(self): + """ + Return a list of all custom place hierarchy types assocated with Place + instances in the database. + """ + return self.db.get_placehier_types() + + def get_placeabbr_types(self): + """ + Return a list of all custom place name types assocated with Place + instances in the database. + """ + return self.db.get_placeabbr_types() + + def get_place_attribute_types(self): + """ + Return a list of all custom place name types assocated with Place + instances in the database. + """ + return self.db.get_place_attribute_types() + def get_repository_types(self): """returns a list of all custom repository types associated with Repository instances in the database""" diff --git a/gramps/gen/proxy/referencedbyselection.py b/gramps/gen/proxy/referencedbyselection.py index 4b678ccb3a6..6fed5b702b6 100644 --- a/gramps/gen/proxy/referencedbyselection.py +++ b/gramps/gen/proxy/referencedbyselection.py @@ -265,12 +265,26 @@ def process_place(self, place): self.process_notes(place) self.process_media_ref_list(place) self.process_urls(place) + self.process_attributes(place) for placeref in place.get_placeref_list(): + self.process_citation_ref_list(placeref) place = self.db.get_place_from_handle(placeref.ref) if place: self.process_place(place) + for event_ref in place.get_event_ref_list(): + if event_ref: + event = self.db.get_event_from_handle(event_ref.ref) + if event: + self.process_event_ref(event_ref) + + for name in place.get_names(): + self.process_citation_ref_list(name) + + for ptype in place.get_types(): + self.process_citation_ref_list(ptype) + self.process_tags(place) def process_source(self, source): diff --git a/gramps/gui/editors/filtereditor.py b/gramps/gui/editors/filtereditor.py index d4ec6cf6db9..505ac8f3732 100644 --- a/gramps/gui/editors/filtereditor.py +++ b/gramps/gui/editors/filtereditor.py @@ -71,7 +71,7 @@ from gramps.gen.display.place import displayer as _pd from gramps.gen.utils.db import family_name from gramps.gen.utils.string import conf_strings -from ..widgets import DateEntry +from ..widgets import DateEntry, PlaceTypeSelector from gramps.gen.datehandler import displayer from gramps.gen.config import config @@ -101,6 +101,7 @@ _name2typeclass = { _('Personal event:') : EventType, _('Family event:') : EventType, + _('Place event:') : EventType, _('Event type:') : EventType, _('Personal attribute:') : AttributeType, _('Family attribute:') : AttributeType, @@ -110,7 +111,6 @@ _('Note type:') : NoteType, _('Name type:') : NameType, _('Surname origin type:'): NameOriginType, - _('Place type:') : PlaceType, } #------------------------------------------------------------------------- @@ -405,11 +405,11 @@ def __init__(self, type_class, additional): #we need to inherit and have an combobox with an entry Gtk.ComboBox.__init__(self, has_entry=True) self.type_class = type_class - self.sel = StandardCustomSelector(type_class._I2SMAP, self, - type_class._CUSTOM, + self.sel = StandardCustomSelector(type_class.get_map(type_class), self, + type_class.CUSTOM, type_class._DEFAULT, additional, - type_class._MENU) + type_class.get_menu(type_class)) self.show() def get_text(self): @@ -420,6 +420,42 @@ def set_text(self, val): tc.set_from_xml_str(val) self.sel.set_values((int(tc), str(tc))) + +#------------------------------------------------------------------------- +# +# MyPlaceTypeSelect +# +#------------------------------------------------------------------------- +class MyPlaceTypeSelect(Gtk.ComboBox): + """ + for filters, we store the pt_id, or if CUSTOM, the name of the PlaceType + in the filter rule. + + The PlaceTypeSelector assumes an actual PlaceType. + + This handles the conversion back and forth. + """ + def __init__(self, dbstate): + #we need to inherit and have an combobox with an entry + self.dbstate = dbstate + Gtk.ComboBox.__init__(self, has_entry=True) + self.ptype = PlaceType(PlaceType.CUSTOM) # for add rule default + self.sel = PlaceTypeSelector(dbstate, self, self.ptype) + self.show() + + def set_text(self, text): + """ allows the rule to preset the PlaceType string from stored handle + """ + self.ptype.set(text) + self.sel.update() + + def get_text(self): + """ retrieve the pt_id for the filter rule """ + if self.ptype.is_custom(): + return self.ptype.name + return self.ptype.pt_id + + #------------------------------------------------------------------------- # # MyEntry @@ -561,10 +597,9 @@ def __init__(self, namespace, dbstate, uistate, track, filterdb, val, additional = self.db.get_name_types() elif v == _('Surname origin type:'): additional = self.db.get_origin_types() - elif v == _('Place type:'): - additional = sorted(self.db.get_place_types(), - key=lambda s: s.lower()) t = MySelect(_name2typeclass[v], additional) + elif v == _('Place type:'): + t = MyPlaceTypeSelect(self.dbstate) elif v == _('Inclusive:'): t = MyBoolean(_('Include selected Gramps ID')) elif v == _('Case sensitive:'): diff --git a/gramps/gui/filters/sidebar/_placesidebarfilter.py b/gramps/gui/filters/sidebar/_placesidebarfilter.py index 1ac6fe0aa72..02b162ed0be 100644 --- a/gramps/gui/filters/sidebar/_placesidebarfilter.py +++ b/gramps/gui/filters/sidebar/_placesidebarfilter.py @@ -42,9 +42,10 @@ # #------------------------------------------------------------------------- from ... import widgets -from gramps.gen.lib import Place, PlaceType +from gramps.gen.lib import Place from .. import build_filter_model from . import SidebarFilter +from gramps.gui.widgets.placetypeselector import PlaceTypeSelector from gramps.gen.filters import GenericFilterFactory, rules from gramps.gen.filters.rules.place import (RegExpIdOf, HasData, IsEnclosedBy, HasTag, HasNoteRegexp, WithinArea, @@ -64,22 +65,13 @@ def __init__(self, dbstate, uistate, clicked): self.filter_id = widgets.BasicEntry() self.filter_name = widgets.BasicEntry() self.filter_place = Place() - self.filter_place.set_type((PlaceType.CUSTOM, '')) + self.filter_place.set_type('') self.ptype = Gtk.ComboBox(has_entry=True) self.dbstate = dbstate - if dbstate.is_open(): - self.custom_types = dbstate.db.get_place_types() - else: - self.custom_types = [] - - self.place_menu = widgets.MonitoredDataType( - self.ptype, - self.filter_place.set_type, - self.filter_place.get_type, - False, # read-only - self.custom_types - ) - self.filter_code = widgets.BasicEntry() + + self.place_type = PlaceTypeSelector( + self.dbstate, self.ptype, self.filter_place.get_type(), + sidebar=True) self.filter_enclosed = widgets.PlaceEntry(dbstate, uistate, []) self.filter_note = widgets.BasicEntry() self.filter_within = widgets.PlaceWithin(dbstate, uistate, []) @@ -90,6 +82,11 @@ def __init__(self, dbstate, uistate, clicked): SidebarFilter.__init__(self, dbstate, uistate, "Place") + def on_db_changed(self, _db): + self.dbstate.db.connect("custom-type-changed", + self.place_type.fill_models) + self.place_type.fill_models() + def create_widget(self): cell = Gtk.CellRendererText() cell.set_property('width', self._FILTER_WIDTH) @@ -107,7 +104,6 @@ def create_widget(self): self.add_text_entry(_('ID'), self.filter_id) self.add_text_entry(_('Name'), self.filter_name) self.add_entry(_('Type'), self.ptype) - self.add_text_entry(_('Code'), self.filter_code) self.add_text_entry(_('Enclosed By'), self.filter_enclosed) self.add_text_entry(_('Within'), self.filter_within) self.add_text_entry(_('Note'), self.filter_note) @@ -118,7 +114,6 @@ def create_widget(self): def clear(self, obj): self.filter_id.set_text('') self.filter_name.set_text('') - self.filter_code.set_text('') self.filter_enclosed.set_text('') self.filter_note.set_text('') self.filter_within.set_value('', 0) @@ -129,8 +124,8 @@ def clear(self, obj): def get_filter(self): gid = str(self.filter_id.get_text()).strip() name = str(self.filter_name.get_text()).strip() - ptype = self.filter_place.get_type().xml_str() - code = str(self.filter_code.get_text()).strip() + ptype = self.filter_place.get_type() + ptype = ptype.name if ptype.is_custom() else ptype.pt_id enclosed = str(self.filter_enclosed.get_text()).strip() note = str(self.filter_note.get_text()).strip() within = self.filter_within.get_value() @@ -138,8 +133,8 @@ def get_filter(self): tag = self.tag.get_active() > 0 gen = self.generic.get_active() > 0 - empty = not (gid or name or ptype or code or enclosed or note or regex - or within[0] or tag or gen) + empty = not (gid or name or ptype or enclosed or note or regex or + within[0] or tag or gen) if empty: generic_filter = None else: @@ -152,7 +147,7 @@ def get_filter(self): rule = IsEnclosedBy([enclosed, '0']) generic_filter.add_rule(rule) - rule = HasData([name, ptype, code], use_regex=regex) + rule = HasData([name, ptype], use_regex=regex) generic_filter.add_rule(rule) if note: From 1e7dcfc935949bed0155298ac6174d339ff0c27b Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 09:53:36 -0500 Subject: [PATCH 09/26] Import/Export for GEPS045 --- data/tests/exp_sample.gw | 2 +- data/tests/exp_sample.vcs | 32 +-- data/tests/exp_sample.wft | 2 +- data/tests/imp_sample_csv.csv | 4 +- data/tests/imp_sample_csv.gramps | 265 ++++++++++-------- gramps/plugins/export/exportcsv.py | 31 +- gramps/plugins/export/exportgeneweb.py | 8 +- gramps/plugins/export/exportvcalendar.py | 2 +- gramps/plugins/importer/importcsv.py | 38 +-- .../plugins/importer/test/importvcard_test.py | 32 ++- gramps/plugins/lib/libprogen.py | 5 +- gramps/plugins/test/exports_test.py | 9 +- 12 files changed, 238 insertions(+), 192 deletions(-) diff --git a/data/tests/exp_sample.gw b/data/tests/exp_sample.gw index 6cbcccb7711..d8cfd4d90a7 100644 --- a/data/tests/exp_sample.gw +++ b/data/tests/exp_sample.gw @@ -28,7 +28,7 @@ beg - h Eric_Lloyd.11 28/8/1963 #bp San_Francisco,_San_Francisco_Co.,_CA - h Keith_Lloyd.12 11/8/1966 #bp San_Francisco,_San_Francisco_Co.,_CA - h Craig_Peter.13 >1966 #bp San_Francisco,_San_Francisco_Co.,_CA -- h The.14 Tester 29/12/1954 #bp 123_High_St,_Cleveland,_Cuyahoga,_Ohio,_USA +- h The.14 Tester 29/12/1954 #bp 123_High_St,_Cleveland,_Cuyahoga,_Ohio,_USA #dp Houston,_Harris,_Texas end fam Smith Hans_Peter.8 + #nm Jones Lillie_Harriet.15 2/5/1910 #bp Rnne,_Bornholm,_Denmark 26/6/1990 diff --git a/data/tests/exp_sample.vcs b/data/tests/exp_sample.vcs index b8a41b8bdf4..10716f4f990 100644 --- a/data/tests/exp_sample.vcs +++ b/data/tests/exp_sample.vcs @@ -29,7 +29,7 @@ DTSTART:19990811T000001 DTEND:19990811T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160604T213156Z +DTSTAMP:20170929T211825Z UID:0000000c0000000c@gramps.com SUMMARY:1904 Smith, Hans Peter - Birth LOCATION:Rønne, Bornholm, Denmark @@ -56,7 +56,7 @@ DTSTART:19990129T000001 DTEND:19990129T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000001600000016@gramps.com SUMMARY:1889 Nielsen, Herman Julius - Birth LOCATION:Rønne, Bornholm, Denmark @@ -74,7 +74,7 @@ DTSTART:19991104T000001 DTEND:19991104T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000002100000021@gramps.com SUMMARY:1897 Smith, Gus - Birth LOCATION:Rønne, Bornholm, Denmark @@ -92,7 +92,7 @@ DTSTART:19991021T000001 DTEND:19991021T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000002400000024@gramps.com SUMMARY:1907 Anderson, Jennifer - Birth LOCATION:Rønne, Bornholm, Denmark @@ -110,7 +110,7 @@ DTSTART:19990529T000001 DTEND:19990529T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000002700000027@gramps.com SUMMARY:1910 Jones, Lillie Harriet - Birth LOCATION:Rønne, Bornholm, Denmark @@ -154,7 +154,7 @@ DTSTART:19990412T000001 DTEND:19990412T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000003700000037@gramps.com SUMMARY:1899 Smith, Carl Emil - Birth LOCATION:Rønne, Bornholm, Denmark @@ -172,7 +172,7 @@ DTSTART:19990128T000001 DTEND:19990128T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000003a0000003a@gramps.com SUMMARY:1893 Smith, Hjalmar - Birth LOCATION:Rønne, Bornholm, Denmark @@ -181,7 +181,7 @@ DTSTART:19990131T000001 DTEND:19990131T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000003b0000003b@gramps.com SUMMARY:1894 Smith, Hjalmar - Death LOCATION:Rønne, Bornholm, Denmark @@ -199,7 +199,7 @@ DTSTART:19991119T000001 DTEND:19991119T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000004400000044@gramps.com SUMMARY:1889 Smith, Astrid Shermanna Augusta - Birth LOCATION:Rønne, Bornholm, Denmark @@ -226,7 +226,7 @@ DTSTART:19991128T000001 DTEND:19991128T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000005100000051@gramps.com SUMMARY:1886 Smith, Kirsti Marie - Birth LOCATION:Rønne, Bornholm, Denmark @@ -253,7 +253,7 @@ DTSTART:19990923T000001 DTEND:19990923T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000005900000059@gramps.com SUMMARY:1927 Streiffert, Anna - Death LOCATION:Rønne, Bornholm, Denmark @@ -271,7 +271,7 @@ DTSTART:19991006T000001 DTEND:19991006T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000006200000062@gramps.com SUMMARY:1910 Smith, Magnes - Death LOCATION:Rønne, Bornholm, Denmark @@ -405,7 +405,7 @@ DTSTART:19990626T000001 DTEND:19990626T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:0000009d0000009d@gramps.com SUMMARY:1895 Smith, Hjalmar - Birth LOCATION:Rønne, Bornholm, Denmark @@ -441,7 +441,7 @@ DTSTART:19991229T000001 DTEND:19991229T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:000000b1000000b1@gramps.com SUMMARY:1885 Smith, Gustaf Sr. and Hansdotter, Anna - Marriage LOCATION:Rønne, Bornholm, Denmark @@ -459,7 +459,7 @@ DTSTART:19990810T000001 DTEND:19990810T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:000000b3000000b3@gramps.com SUMMARY:1912 Nielsen, Herman Julius and Smith, Astrid Shermanna Augusta - M arriage @@ -505,7 +505,7 @@ DTSTART:19990527T000001 DTEND:19990527T235959 END:VEVENT BEGIN:VEVENT -DTSTAMP:20160522T151317Z +DTSTAMP:20170929T211825Z UID:000000a9000000a9@gramps.com SUMMARY:1884 Smith, Magnes and Streiffert, Anna - Marriage LOCATION:Rønne, Bornholm, Denmark diff --git a/data/tests/exp_sample.wft b/data/tests/exp_sample.wft index 323cd3c31f9..03aa57dfa74 100644 --- a/data/tests/exp_sample.wft +++ b/data/tests/exp_sample.wft @@ -42,7 +42,7 @@ Hjalmar Smith0;Gustaf Smith;Anna Hansdotter;;;7/4/1895-26/6/1975 Emil Smith;Martin Smith;Kerstina Hansdotter;;;27/9/1860 雪 Ke 柯;Herman Nielsen;Astrid Smith;;; ピーター リチミシキスイミ;;;;; -The Tester;Lloyd Smith;Janis Green;;;29/12/1954 +The Tester;Lloyd Smith;Janis Green;;;29/12/1954- Mrs Tester;;;;; Tom Von Tester y tested;The Tester;Mrs Tester;;; Fake von Person, I;The Tester;Mrs Tester;;;1954- diff --git a/data/tests/imp_sample_csv.csv b/data/tests/imp_sample_csv.csv index 79e9f9a3407..891ddbbd2a2 100644 --- a/data/tests/imp_sample_csv.csv +++ b/data/tests/imp_sample_csv.csv @@ -26,7 +26,7 @@ [P0027],"Sacramento Co., California, USA",Sacramento Co.,County,,,,[P0028],2016-06-01,,,,,,,,,,,,, [P0015],"Sacramento, Sacramento Co., CA","Sacramento, Sacramento Co., CA",City,,,,[P0027],2016-06-04,,,,,,,,,,,,, [P0024],"Denver Co., Colorado, USA",Denver Co.,County,,,,[P0025],,,,,,,,,,,,,, -[P0014],"Denver, Denver Co., CO","Denver, Denver Co., CO",City,39.7392,104.9903 W,,[P0024],,,,,,,,,,,,,, +[P0014],"Denver, Denver Co., CO","Denver",City,39.7392,104.9903 W,,[P0024],,,,,,,,,,,,,, L18,UC Berkeley,UC Berkeley,Unknown,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,, Person,Surname,Given,Call,Suffix,Prefix,Title,Gender,Birth date,Birth place,birthplaceid,Birth source,Baptism date,Baptism place,Baptism source,Death date,Death place,Death source,Burial date,Burial place,Burial source,Note @@ -41,7 +41,7 @@ Person,Surname,Given,Call,Suffix,Prefix,Title,Gender,Birth date,Birth place,birt [I0017],Jones,Lillie Harriet,,,,,female,2 May 1910,"Rønne, Bornholm, Denmark",,,,,,26-Jun-90,,,,,, [I0013],Michaels,Evelyn,,,,,female,about 1897,,,,,,,,,,,,, [I0012],Nielsen,Herman Julius,,,,,male,31 Aug 1889,"Rønne, Bornholm, Denmark",,,,,,1945,,,,,, -[I0031],Ohman,Marjorie,,,,,female,3 Jun 1903,"Denver, Denver Co., CO, Denver Co., Colorado, USA",,,,,,22-Jun-80,"Reno, Washoe Co., NV",,,,, +[I0031],Ohman,Marjorie,,,,,female,3 Jun 1903,"Denver, Denver Co., Colorado, USA",,,,,,22-Jun-80,"Reno, Washoe Co., NV",,,,, [I0034],Perkins,Alice Paula,,,,,female,22 Nov 1933,"Sparks, Washoe Co., NV",,,,,,,,,,,, [I0002],Smith,Amber Marie,,,,,female,12 Apr 1998,"Hayward, Alameda Co., CA",,,,,,,,,,,, [I0023],Smith,Astrid Shermanna Augusta,,,,,female,31 Jan 1889,"Rønne, Bornholm, Denmark",,,,,,21-Dec-63,"San Francisco, San Francisco Co., CA",,,,, diff --git a/data/tests/imp_sample_csv.gramps b/data/tests/imp_sample_csv.gramps index b981ab7923a..d393065ffaf 100644 --- a/data/tests/imp_sample_csv.gramps +++ b/data/tests/imp_sample_csv.gramps @@ -1,393 +1,393 @@ - - + +
- +
- + Birth - + Birth - + Death - + Birth - + Birth - + Birth - + Death - + Birth - + Death - + Birth - + Birth - + Death - + Birth - + Death - + Birth - + Birth - + Death - + Birth - + Death - + Birth - + Birth - + Birth - + Death - + Birth - + Death - + Birth - + Birth - + Birth - + Birth - + Birth - + Death - + Birth - + Death - + Birth - + Birth - + Death - + Burial - + Birth - + Death - + Birth - + Baptism - + Death - + Birth - + Birth - + Birth - + Birth - + Birth - + Birth - + Death - + Birth - + Birth - + Birth - + Death - + Birth - + Birth - + Birth - + Baptism - + Death - + Birth - + Death - + Birth - + Birth - + Death - + Birth - + Birth - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage - + Marriage @@ -979,134 +979,163 @@ - + Löderup, Malmöhus Län, Sweden + - + Sparks, Washoe Co., NV + - + San Francisco, San Francisco Co., CA + - + Rønne, Bornholm, Denmark + - + Gladsax, Kristianstad Län, Sweden + - + Reno, Washoe Co., NV + - + Hayward, Alameda Co., CA + - + Community Presbyterian Church, Danville, CA + - + Sweden + - + Grostorp, Kristianstad Län, Sweden + - + Copenhagen, Denmark + - + Hoya/Jona/Hoia, Sweden + - + Simrishamn, Kristianstad Län, Sweden + - + Fremont, Alameda Co., CA + - + Santa Rosa, Sonoma Co., CA + - + San Jose, Santa Clara Co., CA + - + Smestorp, Kristianstad Län, Sweden + - + Tommarp, Kristianstad Län, Sweden + - + Rønne Bornholm, Denmark + - + Woodland, Yolo Co., CA + - + San Ramon, Conta Costa Co., CA + - + United States of America + - + California, USA - + + - + Colorado, USA - + + - + Sacramento Co., California, USA - + + - + Sacramento, Sacramento Co., CA - + + - + Denver Co., Colorado, USA - + + - + Denver, Denver Co., CO - + + - + - + UC Berkeley +
diff --git a/gramps/plugins/export/exportcsv.py b/gramps/plugins/export/exportcsv.py index 183e05c14cf..a23ba6b562c 100644 --- a/gramps/plugins/export/exportcsv.py +++ b/gramps/plugins/export/exportcsv.py @@ -51,9 +51,10 @@ # Gramps modules # #------------------------------------------------------------------------- -from gramps.gen.lib import EventType, Person +from gramps.gen.lib import EventType, Person, AttributeType from gramps.gen.lib.eventroletype import EventRoleType from gramps.gui.plug.export import WriterOptionBox +from gramps.gen.utils.location import get_code from gramps.gen.utils.string import gender as gender_map from gramps.gen.datehandler import get_date from gramps.gen.display.place import displayer as _pd @@ -295,30 +296,34 @@ def export_data(self): if place: place_id = place.gramps_id place_title = place.title - place_name = place.name.value - place_type = str(place.place_type) + place_name = place.get_name().value + place_type = str(place.get_type()) place_latitude = place.lat place_longitude = place.long - place_code = place.code + place_code = get_code(place) if place.placeref_list: for placeref in place.placeref_list: - placeref_obj = self.db.get_place_from_handle(placeref.ref) + placeref_obj = self.db.get_place_from_handle( + placeref.ref) placeref_date = "" - if not placeref.date.is_empty(): - placeref_date = placeref.date + if not placeref.get_date_object().is_empty(): + placeref_date = placeref.get_date_object() placeref_id = "" if placeref_obj: placeref_id = "[%s]" % placeref_obj.gramps_id - self.write_csv("[%s]" % place_id, place_title, place_name, place_type, - place_latitude, place_longitude, place_code, placeref_id, + self.write_csv("[%s]" % place_id, place_title, + place_name, place_type, + place_latitude, place_longitude, + place_code, placeref_id, placeref_date) else: - self.write_csv("[%s]" % place_id, place_title, place_name, place_type, - place_latitude, place_longitude, place_code, "", - "") + self.write_csv("[%s]" % place_id, place_title, + place_name, place_type, + place_latitude, place_longitude, + place_code, "", "") self.update() self.writeln() - ########################### sort: + # sort: sortorder = [] dropped_surnames = set() for key in self.plist: diff --git a/gramps/plugins/export/exportgeneweb.py b/gramps/plugins/export/exportgeneweb.py index ee54686b680..5a7dc094cb9 100644 --- a/gramps/plugins/export/exportgeneweb.py +++ b/gramps/plugins/export/exportgeneweb.py @@ -273,7 +273,7 @@ def get_full_person_info(self, person): b_date = self.format_date( birth.get_date_object()) place_handle = birth.get_place_handle() if place_handle: - b_place = _pd.display_event(self.db, birth) + b_place = _pd.display_event(self.db, birth, fmt=0) if probably_alive(person,self.db): d_date = "" @@ -287,7 +287,7 @@ def get_full_person_info(self, person): d_date = self.format_date( death.get_date_object()) place_handle = death.get_place_handle() if place_handle: - d_place = _pd.display_event(self.db, death) + d_place = _pd.display_event(self.db, death, fmt=0) retval = retval + "%s " % b_date if b_place != "": @@ -373,14 +373,14 @@ def get_wedding_data(self,family): m_date = self.format_date( event.get_date_object()) place_handle = event.get_place_handle() if place_handle: - m_place = _pd.display_event(self.db, event) + m_place = _pd.display_event(self.db, event, fmt=0) m_source = self.get_primary_source( event.get_citation_list()) if event.get_type() == EventType.ENGAGEMENT: engaged = 1 eng_date = self.format_date( event.get_date_object()) place_handle = event.get_place_handle() if place_handle: - eng_place = _pd.display_event(self.db, event) + eng_place = _pd.display_event(self.db, event, fmt=0) eng_source = self.get_primary_source( event.get_citation_list()) if event.get_type() == EventType.DIVORCE: divorced = 1 diff --git a/gramps/plugins/export/exportvcalendar.py b/gramps/plugins/export/exportvcalendar.py index 4c2d031f401..50fc7cde67f 100644 --- a/gramps/plugins/export/exportvcalendar.py +++ b/gramps/plugins/export/exportvcalendar.py @@ -210,7 +210,7 @@ def write_vevent(self, event_text, event): self.writeln("UID:%s@gramps.com" % event.handle) self.writeln(fold("SUMMARY:%s %s" % (date.get_year(), event_text))) if place_handle: - location = _pd.display_event(self.db, event) + location = _pd.display_event(self.db, event, fmt=0) if location: self.writeln("LOCATION:%s" % location) self.writeln("RRULE:FREQ=YEARLY") diff --git a/gramps/plugins/importer/importcsv.py b/gramps/plugins/importer/importcsv.py index 3969e00dddc..6632ef42122 100644 --- a/gramps/plugins/importer/importcsv.py +++ b/gramps/plugins/importer/importcsv.py @@ -59,7 +59,6 @@ from gramps.gen.datehandler import parser as _dp from gramps.gen.utils.string import gender as gender_map from gramps.gen.utils.id import create_id -from gramps.gen.utils.location import located_in from gramps.gen.utils.unknown import create_explanation_note from gramps.gen.lib.eventroletype import EventRoleType from gramps.gen.config import config @@ -147,17 +146,6 @@ def __init__(self, dbase, user, default_tag_format=None): self.pref = {} # person ref, internal to this sheet self.fref = {} # family ref, internal to this sheet self.placeref = {} - self.place_types = {} - # Build reverse dictionary, name to type number - for items in PlaceType().get_map().items(): # (0, 'Custom') - self.place_types[items[1]] = items[0] - self.place_types[items[1].lower()] = items[0] - if _(items[1]) != items[1]: - self.place_types[_(items[1])] = items[0] - # Add custom types: - for custom_type in self.db.get_place_types(): - self.place_types[custom_type] = 0 - self.place_types[custom_type.lower()] = 0 column2label = { "surname": ("lastname", "last_name", "surname", _("surname"), _("Surname")), @@ -386,7 +374,7 @@ def _check_refs(self): expl_note = create_explanation_note(self.db) for key in self.placeref: place = self.placeref[key] - if place.name.value == _("Unknown"): + if place.get_name().value == _("Unknown"): txt = (', ' + key) if txt else key place.add_note(expl_note.handle) self.db.commit_place(place, self.trans) @@ -888,21 +876,25 @@ def _parse_place(self, line_number, row, col): if place_title is not None: place.title = place_title if place_name is not None: - place.name = PlaceName(value=place_name) + place.add_name(PlaceName(value=place_name)) if place_type_str is not None: - place.place_type = self.get_place_type(place_type_str) + place.set_type(PlaceType(place_type_str)) + place.set_group(place.get_type().get_probable_group()) if place_latitude is not None: place.lat = place_latitude if place_longitude is not None: place.long = place_longitude if place_code is not None: - place.code = place_code + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(place_code) + place.add_attribute(attr) if place_enclosed_by_id is not None: place_enclosed_by = self.lookup("place", place_enclosed_by_id) if place_enclosed_by is None: # Not yet found in import, so store for later place_enclosed_by = self.create_place() - place_enclosed_by.name.set_value(_('Unknown')) + place_enclosed_by.get_name().set_value(_('Unknown')) if(place_enclosed_by_id.startswith("[") and place_enclosed_by_id.endswith("]")): place_enclosed_by.gramps_id = self.db.pid2user_format( @@ -917,16 +909,10 @@ def _parse_place(self, line_number, row, col): place.placeref_list.append(placeref) if place_date: placeref.date = _dp.parse(place_date) + placeref.set_type_for_place(place_enclosed_by) ######################################################### self.db.commit_place(place, self.trans) - def get_place_type(self, place_type_str): - if place_type_str in self.place_types: - return PlaceType((self.place_types[place_type_str], place_type_str)) - else: - # New custom type: - return PlaceType((0, place_type_str)) - def get_or_create_family(self, family_ref, husband, wife): "Return the family object for the give family ID." # if a gramps_id and exists: @@ -1067,12 +1053,12 @@ def get_or_create_place(self, place_name): LOG.debug("get_or_create_place: looking for: %s", place_name) for place_handle in self.db.iter_place_handles(): place = self.db.get_place_from_handle(place_handle) - place_title = place_displayer.display(self.db, place) + place_title = place_displayer.display(self.db, place, fmt=0) if place_title == place_name: return (0, place) place = Place() place.set_title(place_name) - place.name = PlaceName(value=place_name) + place.set_name(PlaceName(value=place_name)) self.db.add_place(place, self.trans) self.place_count += 1 return (1, place) diff --git a/gramps/plugins/importer/test/importvcard_test.py b/gramps/plugins/importer/test/importvcard_test.py index d51ec647858..466a7f33870 100644 --- a/gramps/plugins/importer/test/importvcard_test.py +++ b/gramps/plugins/importer/test/importvcard_test.py @@ -57,7 +57,9 @@ def setUp(self): def canonicalize(self, doc): handles = {} + people = None for element in doc.iter("*"): + #print(element.tag) gramps_id = element.get('id') if gramps_id is not None: handles[element.get('handle')] = gramps_id @@ -67,10 +69,28 @@ def canonicalize(self, doc): element.set('hlink', handles.get(hlink)) if element.get('change') is not None: del element.attrib['change'] + if 'place-types' in element.tag or 'researcher' in element.tag: + element.clear() + continue if element.text is not None and not element.text.strip(): element.text = '' if element.tail is not None and not element.tail.strip(): element.tail = '' + if 'people' in element.tag: + people = element + # Grramps XML is sorted by handle for its records. Unfortuantely, + # this is not always consistant when several records are created + # in the same second. The lower half of the handle is random and can + # result in different handle order, which messes up this test. + # So re-sort the people by id instead. + if people: + data = [] + for pers in people: + key = pers.get('id') + data.append((key, pers)) + data.sort() + # insert the last item from each tuple + people[:] = [item[-1] for item in data] return ET.tostring(doc, encoding='utf-8') @@ -93,11 +113,13 @@ def do_case(self, input_str, expect_doc, debug=False): print(err_str) result_doc = ET.XML(result_str) - if debug: - print(self.canonicalize(result_doc)) - print(self.canonicalize(expect_doc)) - self.assertEqual(self.canonicalize(result_doc), - self.canonicalize(expect_doc)) + res = self.canonicalize(result_doc) + exp = self.canonicalize(expect_doc) + if res != exp: + print() + print(res) + print(exp) + self.assertEqual(res, exp) def test_base(self): self.do_case("\r\n".join(self.vcard), self.gramps) diff --git a/gramps/plugins/lib/libprogen.py b/gramps/plugins/lib/libprogen.py index fb1c60fdbc5..518c1e4c6e0 100644 --- a/gramps/plugins/lib/libprogen.py +++ b/gramps/plugins/lib/libprogen.py @@ -54,8 +54,8 @@ from gramps.gen.lib import (Address, Attribute, AttributeType, ChildRef, Citation, Date, Event, EventRef, EventType, Family, FamilyRelType, Name, NameType, NameOriginType, Note, - NoteType, Person, Place, PlaceName, Source, - SrcAttribute, Surname, Tag) + NoteType, Person, Place, PlaceName, PlaceType, + Source, SrcAttribute, Surname, Tag) from gramps.gen.updatecallback import UpdateCallback from gramps.gen.utils.id import create_id @@ -844,6 +844,7 @@ def __get_or_create_place(self, place_name): place = Place() place.set_name(PlaceName(value=place_name)) place.set_title(place_name) + place.group = place.get_type().get_probable_group() self.__add_tag('place', place) # add tag to 'Place' self.dbase.add_place(place, self.trans) # add & commit ... diff --git a/gramps/plugins/test/exports_test.py b/gramps/plugins/test/exports_test.py index 587b1e1ba49..e4899f4eeff 100644 --- a/gramps/plugins/test/exports_test.py +++ b/gramps/plugins/test/exports_test.py @@ -26,14 +26,14 @@ from time import localtime, strptime from gramps.test.test_util import Gramps -from gramps.gen.const import TEMP_DIR, DATA_DIR +from gramps.gen.const import TEMP_DIR, DATA_DIR, HOME_DIR from gramps.gen.datehandler import set_format from gramps.gen.user import User from gramps.gen.utils.config import config TREE_NAME = "Test_exporttest" TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests")) - +DB_DIR = os.path.join(HOME_DIR, 'grampsdb') def mock_localtime(*args): """ @@ -204,7 +204,10 @@ def setUp(self): # "--import", example) def tearDown(self): - call("-y", "-q", "--remove", TREE_NAME) + dbdir = os.path.join(DB_DIR, TREE_NAME) + if os.path.exists(dbdir): + os.rmdir(dbdir) +# call("-y", "-q", "--remove", TREE_NAME) def test_csv(self): """ Run a csv export test """ From 2ccd6db94d43d2baf202edfc5f94b36e741abf7d Mon Sep 17 00:00:00 2001 From: prculley Date: Tue, 4 Jun 2019 14:07:44 -0500 Subject: [PATCH 10/26] Avoid duplicted attributes on add --- gramps/gen/lib/attrbase.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gramps/gen/lib/attrbase.py b/gramps/gen/lib/attrbase.py index 1e88269d722..7f18d27c39d 100644 --- a/gramps/gen/lib/attrbase.py +++ b/gramps/gen/lib/attrbase.py @@ -80,6 +80,13 @@ def add_attribute(self, attribute): :type attribute: :class:`~.attribute.Attribute` """ assert not isinstance(attribute, str) + for attr in self.attribute_list: + equi = attr.is_equivalent(attribute) + if equi == IDENTICAL: + return + elif equi == EQUAL: + attr.merge(attribute) + return self.attribute_list.append(attribute) def remove_attribute(self, attribute): From 68609a9acb7be25678243bd876b94ab33e604efe Mon Sep 17 00:00:00 2001 From: prculley Date: Thu, 6 Jun 2019 10:14:55 -0500 Subject: [PATCH 11/26] Add Place Attribute Gramplet --- gramps/plugins/gramplet/attributes.py | 29 +++++++++++++++++++++++++ gramps/plugins/gramplet/gramplet.gpr.py | 14 ++++++++++++ gramps/plugins/lib/libplaceview.py | 3 ++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/gramps/plugins/gramplet/attributes.py b/gramps/plugins/gramplet/attributes.py index e50347c46de..a348eb7a242 100644 --- a/gramps/plugins/gramplet/attributes.py +++ b/gramps/plugins/gramplet/attributes.py @@ -257,3 +257,32 @@ def main(self): self.set_has_data(False) else: self.set_has_data(False) + + +class PlaceAttributes(Attributes): + """ + Displays the attributes of a place object. + """ + def db_changed(self): + self.connect(self.dbstate.db, 'place-update', self.update) + self.connect_signal('Place', self.update) + + def update_has_data(self): + active_handle = self.get_active('Place') + if active_handle: + active = self.dbstate.db.get_place_from_handle(active_handle) + self.set_has_data(self.get_has_data(active)) + else: + self.set_has_data(False) + + def main(self): + self.model.clear() + active_handle = self.get_active('Place') + if active_handle: + active = self.dbstate.db.get_place_from_handle(active_handle) + if active: + self.display_attributes(active) + else: + self.set_has_data(False) + else: + self.set_has_data(False) diff --git a/gramps/plugins/gramplet/gramplet.gpr.py b/gramps/plugins/gramplet/gramplet.gpr.py index fa14c978b26..2cda000dcfe 100644 --- a/gramps/plugins/gramplet/gramplet.gpr.py +++ b/gramps/plugins/gramplet/gramplet.gpr.py @@ -674,6 +674,20 @@ navtypes=["Citation"], ) +register(GRAMPLET, + id="Place Attributes", + name=_("Place Attributes"), + description = _("Gramplet showing the attributes of a place object"), + version="1.0.0", + gramps_target_version=MODULE_VERSION, + status = STABLE, + fname="attributes.py", + height=200, + gramplet = 'PlaceAttributes', + gramplet_title=_("Attributes"), + navtypes=["Place"], + ) + register(GRAMPLET, id="Person Notes", name=_("Person Notes"), diff --git a/gramps/plugins/lib/libplaceview.py b/gramps/plugins/lib/libplaceview.py index b54f552ec08..43b3756a8fc 100644 --- a/gramps/plugins/lib/libplaceview.py +++ b/gramps/plugins/lib/libplaceview.py @@ -611,10 +611,11 @@ def get_default_gramplets(self): ("Place Details", "Place Enclosed By", "Place Encloses", - "Place Events", "Place Gallery", "Place Citations", "Place Notes", + "Place Attributes", + "Place Events", "Place Backlinks")) From 6bd5b7652e94312b686a9582e07fe58ed55e9da5 Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 18 Jul 2020 10:08:21 -0500 Subject: [PATCH 12/26] Reports for GEPS045 --- gramps/plugins/drawreport/statisticschart.py | 10 +- gramps/plugins/graph/gvfamilylines.py | 6 +- gramps/plugins/graph/gvrelgraph.py | 6 +- gramps/plugins/lib/libsubstkeyword.py | 76 +++++++++---- gramps/plugins/textreport/ancestorreport.py | 8 +- gramps/plugins/textreport/placereport.py | 48 +++++--- gramps/plugins/webreport/basepage.py | 114 ++++++++++++++----- gramps/plugins/webreport/common.py | 5 +- gramps/plugins/webreport/narrativeweb.py | 17 ++- gramps/plugins/webreport/place.py | 24 ++-- 10 files changed, 231 insertions(+), 83 deletions(-) diff --git a/gramps/plugins/drawreport/statisticschart.py b/gramps/plugins/drawreport/statisticschart.py index 5072e3d00c0..fca07e6c3e0 100644 --- a/gramps/plugins/drawreport/statisticschart.py +++ b/gramps/plugins/drawreport/statisticschart.py @@ -379,6 +379,7 @@ def __init__(self): 'data_etypes': ("Event type", _T_("Event type"), self.get_event_handles, self.get_event_type) } + self._place_format = None # ----------------- data extraction methods -------------------- # take an object and return a list of strings @@ -454,7 +455,8 @@ def get_place(self, event): "return place for given event" place_handle = event.get_place_handle() if place_handle: - place = _pd.display_event(self.db, event) + place = _pd.display_event(self.db, event, + fmt=self._place_format) if place: return [place] return [_T_("Place missing")] @@ -467,7 +469,8 @@ def get_places(self, data): event = self.db.get_event_from_handle(event_handle) place_handle = event.get_place_handle() if place_handle: - place = _pd.display_event(self.db, event) + place = _pd.display_event(self.db, event, + fmt=self._place_format) if place: places.append(place) else: @@ -771,6 +774,7 @@ def __init__(self, database, options, user): get_option_by_name = menu.get_option_by_name get_value = lambda name: get_option_by_name(name).get_value() + _Extract._place_format = get_value("place_format") filter_opt = get_option_by_name('filter') self.filter = filter_opt.get_filter() self.fil_name = "(%s)" % self.filter.get_name(self._locale) @@ -1078,6 +1082,8 @@ def add_menu_options(self, menu): self._nf = stdoptions.add_name_format_option(menu, category_name) self._nf.connect('value-changed', self.__update_filters) + stdoptions.add_place_format_option(menu, category_name) + self.__update_filters() stdoptions.add_private_data_option(menu, category_name) diff --git a/gramps/plugins/graph/gvfamilylines.py b/gramps/plugins/graph/gvfamilylines.py index a87777d7410..d58210a2738 100644 --- a/gramps/plugins/graph/gvfamilylines.py +++ b/gramps/plugins/graph/gvfamilylines.py @@ -173,6 +173,8 @@ def add_menu_options(self, menu): stdoptions.add_name_format_option(menu, category_name) + stdoptions.add_place_format_option(menu, category_name) + stdoptions.add_private_data_option(menu, category_name, default=False) stdoptions.add_living_people_option(menu, category_name) @@ -365,6 +367,7 @@ def __init__(self, database, options, user): self._deleted_families = 0 self._user = user + self._place_format = get_value("place_format") self._followpar = get_value('followpar') self._followchild = get_value('followchild') self._removeextra = get_value('removeextra') @@ -1089,7 +1092,8 @@ def get_event_place(self, event): if place_handle: place = self._db.get_place_from_handle(place_handle) if place: - place_text = _pd.display(self._db, place) + place_text = _pd.display(self._db, place, date=event.date, + fmt=self._place_format) place_text = html.escape(place_text) return place_text diff --git a/gramps/plugins/graph/gvrelgraph.py b/gramps/plugins/graph/gvrelgraph.py index cacbc3f8278..e9a7aae56ea 100644 --- a/gramps/plugins/graph/gvrelgraph.py +++ b/gramps/plugins/graph/gvrelgraph.py @@ -139,6 +139,7 @@ def __init__(self, database, options, user): self.database = CacheProxyDb(self.database) self._db = self.database + self._place_format = get_value("place_format") self.includeid = get_value('inc_id') self.includeurl = get_value('url') self.includeimg = get_value('includeImages') @@ -767,9 +768,10 @@ def get_place_string(self, event): empty string """ if event and self.event_choice in [2, 3, 5, 6, 7]: - place = _pd.display_event(self._db, event) + place = _pd.display_event(self._db, event, fmt=self._place_format) return html.escape(place) return '' + def get_date(self, date): """ return a formatted date """ return html.escape(self._get_date(date)) @@ -850,6 +852,8 @@ def add_menu_options(self, menu): self.__update_filters() + stdoptions.add_place_format_option(menu, category_name) + stdoptions.add_private_data_option(menu, category_name) stdoptions.add_living_people_option(menu, category_name) diff --git a/gramps/plugins/lib/libsubstkeyword.py b/gramps/plugins/lib/libsubstkeyword.py index 3a9d5d34a1a..31b4af3b1ec 100644 --- a/gramps/plugins/lib/libsubstkeyword.py +++ b/gramps/plugins/lib/libsubstkeyword.py @@ -38,9 +38,9 @@ # Gramps modules # #------------------------------------------------------------------------ -from gramps.gen.lib import EventType, PlaceType, Location +from gramps.gen.lib import EventType, PlaceType, PlaceGroupType, Location from gramps.gen.utils.db import get_birth_or_fallback, get_death_or_fallback -from gramps.gen.utils.location import get_main_location +from gramps.gen.utils.location import get_location_list, get_code from gramps.gen.display.place import displayer as _pd from gramps.gen.const import GRAMPS_LOCALE as glocale @@ -336,52 +336,90 @@ class PlaceFormat(GenericFormat): def __init__(self, database, _in): self.database = database GenericFormat.__init__(self, _in) + self.date = None def get_place(self, database, event): """ A helper method for retrieving a place from an event """ if event: bplace_handle = event.get_place_handle() + self.date = event.get_date_object() if bplace_handle: return database.get_place_from_handle(bplace_handle) return None def _default_format(self, place): - return _pd.display(self.database, place) + return _pd.display(self.database, place, date=self.date) def parse_format(self, database, place): - """ Parse the place """ + """ Parse the place + e = street o = phone (doesn't work for places) + l = locality i = parish + c = city t = title (default) + u = county x = longitude + s = state y = latitude + p = postal 1-5 = title (format number from 1-5) + n = country + """ if self.is_blank(place): return code = "elcuspn" + "oitxy" upper = code.upper() + code += "12345" - main_loc = get_main_location(database, place) location = Location() - location.set_street(main_loc.get(PlaceType.STREET, '')) - location.set_locality(main_loc.get(PlaceType.LOCALITY, '')) - location.set_parish(main_loc.get(PlaceType.PARISH, '')) - location.set_city(main_loc.get(PlaceType.CITY, '')) - location.set_county(main_loc.get(PlaceType.COUNTY, '')) - location.set_state(main_loc.get(PlaceType.STATE, '')) - location.set_postal_code(main_loc.get(PlaceType.STREET, '')) - location.set_country(main_loc.get(PlaceType.COUNTRY, '')) + loc_list = get_location_list(database, place, date=self.date) + for loc in loc_list: + # loc_list shoud be in order from smallest to largest + name, place_type, dummy_hndl, abbrs, group = loc + if place_type == "Street": # PlaceType.STREET: + location.set_street(name) + continue + elif place_type == "Locality": # PlaceType.LOCALITY: + location.set_locality(name) + continue + elif place_type == "Parish": # PlaceType.PARISH: + location.set_parish(name) + continue + elif group == PlaceGroupType.COUNTRY and not location.country: + # should find smaller of country group + location.set_country(abbrs[0].value if abbrs else name) + continue + elif group == PlaceGroupType.REGION and not location.county: + # should find smaller of region group (county) + location.set_county(name) + continue + elif group == PlaceGroupType.REGION: + # should find largest (state) + location.set_state(name) + continue + elif group == PlaceGroupType.PLACE: + # should find largest (city) + location.set_city(name) + + def get_title(fmt=-1): + return _pd.display(self.database, place, fmt=fmt) function = [location.get_street, location.get_locality, location.get_city, location.get_county, location.get_state, - place.get_code, + lambda : get_code(place), location.get_country, - location.get_phone, + location.get_phone, # never returns anything location.get_parish, - place.get_title, + get_title, place.get_longitude, - place.get_latitude - ] + place.get_latitude, + lambda : get_title(fmt=0), + lambda : get_title(fmt=1), + lambda : get_title(fmt=2), + lambda : get_title(fmt=3), + lambda : get_title(fmt=4) + ] return self.generic_format(place, code, upper, function) @@ -1451,7 +1489,7 @@ def name_set(): def place_set(): #code = "elcuspnitxy" - main_loc = place_to_test.get_main_location() + main_loc = place_to_test.get_main_location() # TODO main_loc.set_street( "Lost River Ave." if 0 in y_or_n else "" ) diff --git a/gramps/plugins/textreport/ancestorreport.py b/gramps/plugins/textreport/ancestorreport.py index b0d555206c7..c96870e2ec2 100644 --- a/gramps/plugins/textreport/ancestorreport.py +++ b/gramps/plugins/textreport/ancestorreport.py @@ -117,9 +117,11 @@ def __init__(self, database, options, user): raise ReportError(_("Person %s is not in the Database") % pid) stdoptions.run_name_format_option(self, menu) + place_format = menu.get_option_by_name('place_format').get_value() - self.__narrator = Narrator(self.database, use_fulldate=True, - nlocale=self._locale) + self.__narrator = Narrator( + self.database, use_fulldate=True, nlocale=self._locale, + place_format=place_format) def apply_filter(self, person_handle, index, generation=1): """ @@ -311,6 +313,8 @@ def add_menu_options(self, menu): stdoptions.add_name_format_option(menu, category_name) + stdoptions.add_place_format_option(menu, category_name) + stdoptions.add_private_data_option(menu, category_name) stdoptions.add_living_people_option(menu, category_name) diff --git a/gramps/plugins/textreport/placereport.py b/gramps/plugins/textreport/placereport.py index 7cec0e9bfca..b0805ac405f 100644 --- a/gramps/plugins/textreport/placereport.py +++ b/gramps/plugins/textreport/placereport.py @@ -81,7 +81,8 @@ def __init__(self, database, options, user): self._user = user menu = options.menu - self.set_locale(menu.get_option_by_name('trans').get_value()) + self.locale = self.set_locale(menu.get_option_by_name( + 'trans').get_value()) stdoptions.run_date_format_option(self, menu) @@ -182,21 +183,32 @@ def __write_place(self, handle, place_nbr): place_details = [self._("Gramps ID: %s ") % place.get_gramps_id()] for level in get_location_list(self._db, place): # translators: needed for French, ignore otherwise - place_details.append(self._("%(str1)s: %(str2)s" - ) % {'str1': self._(level[1].xml_str()), - 'str2': level[0]}) + place_details.append(_("%(str1)s: %(str2)s") % + {'str1': level[1].str(self.locale), + 'str2': level[0]}) place_names = '' - all_names = place.get_names() - if len(all_names) > 1 or __debug__: - for place_name in all_names: - if place_names != '': - # translators: needed for Arabic, ignore otherwise - place_names += self._(", ") - place_names += '%s' % place_name.get_value() - if place_name.get_language() != '' or __debug__: - place_names += ' (%s)' % place_name.get_language() - place_details += [self._("places|All Names: %s") % place_names,] + for place_name in place.get_names(): + if place_names != '': + # translators: needed for Arabic, ignore otherwise + place_names += self._(", ") + place_names += '\u2028%s' % place_name.get_value() + if place_name.get_language() != '': + place_names += ' (%s)' % place_name.get_language() + if not place_name.get_date_object().is_empty(): + place_names += ' [%s]' % self._get_date( + place_name.get_date_object()) + place_details.append(self._("places|All Names:") + place_names) + place_types = '' + for ptype in place.get_types(): + if place_types != '': + # translators: needed for Arabic, ignore otherwise + place_types += self._(", ") + place_types += '\u2028%s' % str(ptype) + if not ptype.get_date_object().is_empty(): + place_types += ' [%s]' % self._get_date( + ptype.get_date_object()) + place_details.append(self._("places|All Types:") + place_types) self.doc.start_paragraph("PLC-PlaceTitle") place_title = _pd.display(self._db, place, None, self.place_format) self.doc.write_text(("%(nbr)s. %(place)s") % {'nbr' : place_nbr, @@ -248,7 +260,7 @@ def __write_referenced_events(self, handle): for (ref_type, ref_handle) in ref_handles: if ref_type == 'Person': person_list.append(ref_handle) - else: + elif ref_type == 'Family': family = self._db.get_family_from_handle(ref_handle) father = family.get_father_handle() if father: @@ -319,7 +331,7 @@ def __write_referenced_persons(self, handle): person = self._db.get_person_from_handle(ref_handle) name_entry = "%s (%s)" % (self._nd.display(person), person.get_gramps_id()) - else: + elif ref_type == 'Family': family = self._db.get_family_from_handle(ref_handle) f_handle = family.get_father_handle() m_handle = family.get_mother_handle() @@ -349,6 +361,8 @@ def __write_referenced_persons(self, handle): # No parents - bug #7299 continue + else: # ref_type == 'Place' a place event! + continue if name_entry in person_dict: person_dict[name_entry].append(evt_handle) else: @@ -541,7 +555,7 @@ def __place_details_style(self): font.set(face=FONT_SERIF, size=10) para = ParagraphStyle() para.set_font(font) - para.set(first_indent=0.0, lmargin=1.5) + para.set(first_indent=-0.5, lmargin=2.0) para.set_description(_('The style used for details.')) self.default_style.add_paragraph_style("PLC-PlaceDetails", para) diff --git a/gramps/plugins/webreport/basepage.py b/gramps/plugins/webreport/basepage.py index 73c5b3a4402..8fd99e254a2 100644 --- a/gramps/plugins/webreport/basepage.py +++ b/gramps/plugins/webreport/basepage.py @@ -59,7 +59,7 @@ #------------------------------------------------ from gramps.gen.const import GRAMPS_LOCALE as glocale from gramps.gen.lib import (FamilyRelType, NoteType, NameType, Person, - UrlType, Name, PlaceType, EventRoleType, + UrlType, Name, PlaceGroupType, EventRoleType, Source, Attribute, Media, Repository, Event, Family, Citation, Place, Date) from gramps.gen.lib.date import Today @@ -79,7 +79,7 @@ from gramps.plugins.lib.libhtml import Html, xml_lang from gramps.plugins.lib.libhtmlbackend import HtmlBackend, process_spaces from gramps.gen.utils.place import conv_lat_lon -from gramps.gen.utils.location import get_main_location +from gramps.gen.utils.location import get_location_list from gramps.plugins.webreport.common import (_NAME_STYLE_DEFAULT, HTTP, _NAME_STYLE_FIRST, HTTPS, _get_short_name, @@ -148,6 +148,9 @@ def __init__(self, report, title, gid=None): self.rlocale = report.set_locale(lang) self._ = self.rlocale.translation.sgettext self.colon = self._(':') # translators: needed for French, else ignore + # place format options + self.place_format = report.options['place_format'] + if report.options['securesite']: self.secure_mode = HTTPS @@ -761,7 +764,7 @@ def append_to_place_lat_long(self, place, event, place_lat_long): found = any(data[3] == place_handle and data[4] == event_date for data in place_lat_long) if not found: - placetitle = _pd.display(self.r_db, place) + placetitle = _pd.display(self.r_db, place, fmt=self.place_format) latitude = place.get_latitude() longitude = place.get_longitude() if latitude and longitude: @@ -932,7 +935,8 @@ def get_event_data(self, evt, evt_ref, place_hyper = None if place: - place_name = _pd.display(self.r_db, place, evt.get_date_object()) + place_name = _pd.display(self.r_db, place, evt.get_date_object(), + fmt=self.place_format) place_hyper = self.place_link(place_handle, place_name, uplink=uplink) @@ -990,7 +994,8 @@ def dump_ordinance(self, ldsobj, ldssealedtype): if place_handle: place = self.r_db.get_place_from_handle(place_handle) if place: - place_title = _pd.display(self.r_db, place) + place_title = _pd.display(self.r_db, place, + fmt=self.place_format) place_hyper = self.place_link( place_handle, place_title, place.get_gramps_id(), uplink=True) @@ -1913,7 +1918,7 @@ def media_ref_rect_regions(self, handle, linkurl=True): "evt", True) elif classname == "Place": _obj = self.r_db.get_place_from_handle(newhandle) - _name = _pd.display(self.r_db, _obj) + _name = _pd.display(self.r_db, _obj, fmt=self.place_format) if not _name: _name = self._("Unknown") _linkurl = self.report.build_url_fname_html(newhandle, @@ -2609,25 +2614,52 @@ def dump_place(self, place, table): ) tbody += trow - mlocation = get_main_location(self.r_db, place) - for (label, data) in [ - (self._("Street"), mlocation.get(PlaceType.STREET, '')), - (self._("Locality"), mlocation.get(PlaceType.LOCALITY, '')), - (self._("City"), mlocation.get(PlaceType.CITY, '')), - (self._("Church Parish"), - mlocation.get(PlaceType.PARISH, '')), - (self._("County"), mlocation.get(PlaceType.COUNTY, '')), - (self._("State/ Province"), - mlocation.get(PlaceType.STATE, '')), - (self._("Postal Code"), place.get_code()), - (self._("Province"), mlocation.get(PlaceType.PROVINCE, '')), - (self._("Country"), mlocation.get(PlaceType.COUNTRY, ''))]: - if data: - trow = Html("tr") + ( - Html("td", label, class_="ColumnAttribute", inline=True), - Html("td", data, class_="ColumnValue", inline=True) - ) - tbody += trow + mloc = {} + loc_list = get_location_list(self.r_db, place) + for loc in loc_list: + # loc_list shoud be in order from smallest to largest + name, place_type, dummy_hndl, abbrs, group = loc + if place_type == "Street": + mloc[self._("Street")] = name + continue + elif place_type == "Locality": + mloc[self._("Locality")] = name + continue + elif place_type == "Parish": + mloc[self._("Church Parish")] = name + continue + elif (group == PlaceGroupType.COUNTRY and not + mloc.get(self._("Country"))): + # should find smaller of country group + mloc[self._("Country")] = name + continue + elif (group == PlaceGroupType.REGION and not + mloc.get(self._("County"))): + # should find smaller of region group (county) + mloc[self._("County")] = name + continue + elif group == PlaceGroupType.REGION: + # should find largest (state) + mloc[self._("State/ Province")] = name + continue + elif group == PlaceGroupType.PLACE: + # should find largest (city) + mloc[self._("City")] = name + + for (label, data) in mloc.items(): + trow = Html("tr") + ( + Html("td", label, class_="ColumnAttribute", inline=True), + Html("td", data, class_="ColumnValue", inline=True) + ) + tbody += trow + + for attr in place.get_attribute_list(): + trow = Html("tr") + ( + Html("td", self._(attr.type.xml_str()), + class_="ColumnAttribute", inline=True), + Html("td", attr.value, class_="ColumnValue", inline=True) + ) + tbody += trow # display all related locations for placeref in place.get_placeref_list(): @@ -2643,26 +2675,54 @@ def dump_place(self, place, table): ) tbody += trow - altloc = place.get_alternative_names() + altloc = place.get_names() if altloc: tbody += Html("tr") + Html("td", " ", colspan=2) date_msg = self._("Date range in which the name is valid.") trow = Html("tr") + ( Html("th", self._("Alternate Names"), colspan=1, class_="ColumnAttribute", inline=True), + Html("th", date_msg, colspan=1, + class_="ColumnAttribute", inline=True), Html("th", self._("Language"), colspan=1, class_="ColumnAttribute", inline=True), - Html("th", date_msg, colspan=1, + Html("th", self._("Abbreviations"), colspan=1, class_="ColumnAttribute", inline=True), ) tbody += trow for loc in altloc: place_date = self.rlocale.get_date(loc.date) + abbrs = '' + for abbr in loc.get_abbrevs(): + abbrs += (", " if abbrs else '') + abbr.get_value() trow = Html("tr") + ( Html("td", loc.get_value(), class_="ColumnValue", inline=True), + Html("td", place_date, class_="ColumnValue", + inline=True), Html("td", loc.get_language(), class_="ColumnValue", inline=True), + Html("td", abbrs, class_="ColumnValue", + inline=True), + ) + tbody += trow + + ptypes = place.get_types() + if ptypes: + tbody += Html("tr") + Html("td", " ", colspan=2) + date_msg = self._("Date range in which the type is valid.") + trow = Html("tr") + ( + Html("th", self._("Type"), colspan=1, + class_="ColumnAttribute", inline=True), + Html("th", date_msg, colspan=1, + class_="ColumnAttribute", inline=True), + ) + tbody += trow + for ptype in ptypes: + place_date = self.rlocale.get_date(ptype.get_date_object()) + trow = Html("tr") + ( + Html("td", ptype.str(self.rlocale), class_="ColumnValue", + inline=True), Html("td", place_date, class_="ColumnValue", inline=True), ) diff --git a/gramps/plugins/webreport/common.py b/gramps/plugins/webreport/common.py index 5de2b04ee50..650ad72e6df 100644 --- a/gramps/plugins/webreport/common.py +++ b/gramps/plugins/webreport/common.py @@ -924,12 +924,13 @@ def name_to_md5(text): return md5(text.encode('utf-8')).hexdigest() -def get_gendex_data(database, event_ref): +def get_gendex_data(database, event_ref, p_fmt=-1): """ Given an event, return the date and place a strings @param: database -- The database @param: event_ref -- The event reference + @param: p_fmt -- The place format to use for gendex file """ doe = "" # date of event poe = "" # place of event @@ -943,7 +944,7 @@ def get_gendex_data(database, event_ref): if place_handle: place = database.get_place_from_handle(place_handle) if place: - poe = _pd.display(database, place, date) + poe = _pd.display(database, place, date, fmt=p_fmt) return doe, poe def format_date(date): diff --git a/gramps/plugins/webreport/narrativeweb.py b/gramps/plugins/webreport/narrativeweb.py index 82d520ccda8..a1678c22774 100644 --- a/gramps/plugins/webreport/narrativeweb.py +++ b/gramps/plugins/webreport/narrativeweb.py @@ -173,6 +173,9 @@ def __init__(self, database, options, user): # name format options self.name_format = self.options['name_format'] + # place format options + self.place_format = self.options['place_format'] + # include families or not? self.inc_families = self.options['inc_families'] @@ -880,8 +883,10 @@ def _add_place(self, place_handle, bkref_class, bkref_handle, event): else: name = "" if config.get('preferences.place-auto'): - place_name = _pd.display_event(self._db, event) - pplace_name = _pd.display(self._db, place) + place_name = _pd.display_event(self._db, event, + fmt=self.place_format) + pplace_name = _pd.display(self._db, place, + fmt=self.place_format) else: place_name = place.get_title() pplace_name = place_name @@ -1184,10 +1189,12 @@ def write_gendex(self, filep, person): fullname = person.get_primary_name().get_gedcom_name() # get birth info: - dob, pob = get_gendex_data(self._db, person.get_birth_ref()) + dob, pob = get_gendex_data(self._db, person.get_birth_ref(), + p_fmt=self.place_format) # get death info: - dod, pod = get_gendex_data(self._db, person.get_death_ref()) + dod, pod = get_gendex_data(self._db, person.get_death_ref(), + p_fmt=self.place_format) linew = '|'.join((url, surname, fullname, dob, pob, dod, pod)) + '|\n' if self.archive: filep.write(bytes(linew, "utf8")) @@ -1853,7 +1860,7 @@ def __add_report_display(self, menu): stdoptions.add_name_format_option(menu, category_name) - + stdoptions.add_place_format_option(menu, category_name) locale_opt = stdoptions.add_localization_option(menu, category_name) stdoptions.add_date_format_option(menu, category_name, locale_opt) diff --git a/gramps/plugins/webreport/place.py b/gramps/plugins/webreport/place.py index 88c2ef96305..825295a0211 100644 --- a/gramps/plugins/webreport/place.py +++ b/gramps/plugins/webreport/place.py @@ -49,11 +49,11 @@ # Gramps module #------------------------------------------------ from gramps.gen.const import GRAMPS_LOCALE as glocale -from gramps.gen.lib import (PlaceType, Place, PlaceName) +from gramps.gen.lib import (PlaceGroupType as P_G, Place, PlaceName) from gramps.gen.plug.report import Bibliography from gramps.plugins.lib.libhtml import Html from gramps.gen.utils.place import conv_lat_lon -from gramps.gen.utils.location import get_main_location +from gramps.gen.utils.location import get_location_list from gramps.gen.display.place import displayer as _pd #------------------------------------------------ @@ -214,7 +214,19 @@ def placelistpage(self, report, title): if place.get_change_time() > ldatec: ldatec = place.get_change_time() plc_title = pname - main_location = get_main_location(self.r_db, place) + loc_list = get_location_list(self.r_db, place) + state = country = '' + for loc in loc_list: + # loc_list shoud be in order from small to largest + name, place_type, dummy_hndl, _abbrs, group = loc + if group == P_G.COUNTRY and not country: + # should find smaller of country group + country = name + continue + elif group == P_G.REGION: + # should find largest (state) + state = name + continue if plc_title and plc_title != " ": letter = get_index_letter(first_letter(plc_title), @@ -250,10 +262,8 @@ def placelistpage(self, report, title): Html("td", data or " ", class_=colclass, inline=True) for (colclass, data) in [ - ["ColumnState", - main_location.get(PlaceType.STATE, '')], - ["ColumnCountry", - main_location.get(PlaceType.COUNTRY, '')] + ["ColumnState", state], + ["ColumnCountry", country] ] ) From 2b6b7327ec9a9839ff1fe724132b949270deb3bf Mon Sep 17 00:00:00 2001 From: prculley Date: Tue, 18 Jun 2019 10:27:09 -0500 Subject: [PATCH 13/26] Fix Testcasegenerator for enhanced places --- gramps/plugins/test/tools_test.py | 5 ++++- gramps/plugins/tool/testcasegenerator.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/gramps/plugins/test/tools_test.py b/gramps/plugins/test/tools_test.py index 4613d98c39d..81296bb5c98 100644 --- a/gramps/plugins/test/tools_test.py +++ b/gramps/plugins/test/tools_test.py @@ -35,6 +35,9 @@ TREE_NAME = "Test_tooltest" TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests")) const.myrand = random.Random() +if 'GRAMPS_RESOURCES' not in os.environ: + RES_PATH = os.path.abspath(os.path.join(DATA_DIR, "..")) + os.environ['GRAMPS_RESOURCES'] = RES_PATH def call(*args): @@ -138,7 +141,7 @@ def test_tcg_and_check_and_repair(self): "-y", "-a", "tool", "-p", "name=check") expect = ["7 broken child/family links were fixed", "4 broken spouse/family links were fixed", - "1 place alternate name fixed", + "2 place alternate names fixed", "10 media objects were referenced, but not found", "References to 10 media objects were kept", "3 events were referenced, but not found", diff --git a/gramps/plugins/tool/testcasegenerator.py b/gramps/plugins/tool/testcasegenerator.py index 103c2db7a6d..61efa208acb 100644 --- a/gramps/plugins/tool/testcasegenerator.py +++ b/gramps/plugins/tool/testcasegenerator.py @@ -587,7 +587,8 @@ def test_fix_alt_place_names(self): alt_names = [pri_name, alt_name1, alt_name1, PlaceName(), alt_name2, alt_name3] plac.set_name(pri_name) - plac.set_alternative_names(alt_names) + for name in alt_names: + plac.add_name(name) self.db.add_place(plac, self.trans) def test_fix_duplicated_grampsid(self): @@ -1318,6 +1319,13 @@ def create_all_possible_citations(self, c_h_list, name, message): place = Place() place.set_title(message) place.add_citation(_choice(c_h_list)) + pname = PlaceName(value='All Attribute Test') + pname.add_citation(_choice(c_h_list)) + place.add_name(pname) + ptype = PlaceType(value=PlaceType.BOROUGH) + ptype.add_citation(_choice(c_h_list)) + place.add_type(ptype) + place.add_attribute(att) # Place : MediaRef mref = MediaRef() mref.set_reference_handle(med.handle) @@ -1329,6 +1337,7 @@ def create_all_possible_citations(self, c_h_list, name, message): att.add_citation(_choice(c_h_list)) mref.add_attribute(att) place.add_media_reference(mref) + place.add_attribute(att) self.db.add_place(place, self.trans) ref = Repository() @@ -1918,7 +1927,6 @@ def fill_object(self, obj): if isinstance(obj, Place): obj.set_title(self.rand_text(self.LONG)) obj.set_name(PlaceName(value=self.rand_text(self.SHORT))) - obj.set_code(self.rand_text(self.SHORT)) if _randint(0, 1) == 1: if _randint(0, 4) == 1: obj.set_longitude(self.rand_text(self.SHORT)) From 59082a1e8914c83eaa11b8682a2a7f69ac7ba594 Mon Sep 17 00:00:00 2001 From: prculley Date: Tue, 18 Jun 2019 15:22:26 -0500 Subject: [PATCH 14/26] Fix tags.py for crash when XML unsafe tag names are used --- gramps/gui/views/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gramps/gui/views/tags.py b/gramps/gui/views/tags.py index c01a9e17369..143272dc7d3 100644 --- a/gramps/gui/views/tags.py +++ b/gramps/gui/views/tags.py @@ -25,7 +25,7 @@ # #------------------------------------------------------------------------- from bisect import insort_left -from xml.sax.saxutils import escape +from html import escape #------------------------------------------------------------------------- # From 07305cbbb3f0f78f32869132c1c6faf38ad62346 Mon Sep 17 00:00:00 2001 From: prculley Date: Sun, 27 Oct 2019 10:12:47 -0500 Subject: [PATCH 15/26] fix place coordinates to always use English NSEW Issue #11335 --- gramps/gen/db/upgrade.py | 14 +++++++++++--- gramps/gui/editors/editplace.py | 15 ++++++++++++++- gramps/gui/editors/editplaceref.py | 14 +++++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/gramps/gen/db/upgrade.py b/gramps/gen/db/upgrade.py index 1534ccd1848..ffe3b00e74f 100644 --- a/gramps/gen/db/upgrade.py +++ b/gramps/gen/db/upgrade.py @@ -38,9 +38,9 @@ from ..lib import (AttributeType, EventType, MarkerType, NameOriginType, PlaceHierType, Tag) from ..lib.placetype import PlaceType, DM_GRP -from gramps.gen.utils.file import create_checksum -from gramps.gen.utils.id import create_id -from gramps.gui.dialog import (InfoDialog) +from ..utils.place import translate_en_loc +from ..utils.file import create_checksum +from ..utils.id import create_id from .dbconst import (PERSON_KEY, FAMILY_KEY, EVENT_KEY, MEDIA_KEY, PLACE_KEY, REPOSITORY_KEY, CITATION_KEY, SOURCE_KEY, NOTE_KEY, TAG_KEY) @@ -123,6 +123,14 @@ def gramps_upgrade_20(self): attrs = [code_attr] else: attrs = [] + + # get longitude localized E,W translated to English + long = long.replace( + translate_en_loc['E'], 'E').replace(translate_en_loc['W'], 'W') + # get latitude localized N,S translated to English + lat = lat.replace( + translate_en_loc['N'], 'N').replace(translate_en_loc['S'], 'S') + new_place_data = (hndl, gramps_id, title, long, lat, new_prefs, new_names, new_types, eventrefs, alt_loc, urls, media_list, citation_list, note_list, change, diff --git a/gramps/gui/editors/editplace.py b/gramps/gui/editors/editplace.py index 8ba8dcd03e8..33954aa4d04 100644 --- a/gramps/gui/editors/editplace.py +++ b/gramps/gui/editors/editplace.py @@ -48,7 +48,7 @@ MonitoredDataType) from ..widgets.placetypeselector import PlaceTypeSelector from gramps.gen.errors import ValidationError, WindowActiveError -from gramps.gen.utils.place import conv_lat_lon +from gramps.gen.utils.place import conv_lat_lon, translate_en_loc from gramps.gen.display.place import displayer as place_displayer from gramps.gen.config import config from ..dialog import ErrorDialog @@ -166,6 +166,9 @@ def _setup_fields(self): entry = self.top.get_object("lon_entry") entry.set_ltr_mode() + # get E,W translated to local + self.obj.set_longitude(self.obj.get_longitude().replace( + 'E', translate_en_loc['E']).replace('W', translate_en_loc['W'])) self.longitude = MonitoredEntry( entry, self.obj.set_longitude, self.obj.get_longitude, @@ -176,6 +179,9 @@ def _setup_fields(self): entry = self.top.get_object("lat_entry") entry.set_ltr_mode() + # get N,S translated to local + self.obj.set_latitude(self.obj.get_latitude().replace( + 'N', translate_en_loc['N']).replace('S', translate_en_loc['S'])) self.latitude = MonitoredEntry( entry, self.obj.set_latitude, self.obj.get_latitude, @@ -399,6 +405,13 @@ def save(self, *obj): return place_title = place_displayer.display(self.db, self.obj, fmt=0) + # get localized E,W translated to English + self.obj.set_longitude(self.obj.get_longitude().replace( + translate_en_loc['E'], 'E').replace(translate_en_loc['W'], 'W')) + # get localized N,S translated to English + self.obj.set_latitude(self.obj.get_latitude().replace( + translate_en_loc['N'], 'N').replace(translate_en_loc['S'], 'S')) + if not self.obj.handle: with DbTxn(_("Add Place (%s)") % place_title, self.db) as trans: diff --git a/gramps/gui/editors/editplaceref.py b/gramps/gui/editors/editplaceref.py index e0ad1645aa4..907b09783bf 100644 --- a/gramps/gui/editors/editplaceref.py +++ b/gramps/gui/editors/editplaceref.py @@ -40,7 +40,7 @@ from gramps.gen.lib import NoteType, PlaceType, PlaceName, PlaceGroupType from gramps.gen.db import DbTxn from gramps.gen.errors import ValidationError, WindowActiveError -from gramps.gen.utils.place import conv_lat_lon +from gramps.gen.utils.place import conv_lat_lon, translate_en_loc from gramps.gen.display.place import displayer as place_displayer from gramps.gen.config import config from ..dialog import ErrorDialog @@ -179,6 +179,9 @@ def _setup_fields(self): entry = self.top.get_object("lon_entry") entry.set_ltr_mode() + # get E,W translated to local + self.source.set_longitude(self.source.get_longitude().replace( + 'E', translate_en_loc['E']).replace('W', translate_en_loc['W'])) self.longitude = MonitoredEntry( entry, self.source.set_longitude, self.source.get_longitude, @@ -189,6 +192,9 @@ def _setup_fields(self): entry = self.top.get_object("lat_entry") entry.set_ltr_mode() + # get N,S translated to local + self.source.set_latitude(self.source.get_latitude().replace( + 'N', translate_en_loc['N']).replace('S', translate_en_loc['S'])) self.latitude = MonitoredEntry( entry, self.source.set_latitude, self.source.get_latitude, @@ -409,6 +415,12 @@ def save(self, *obj): self.db.placegroup_types.add(PlaceGroupType(str(htype))) place_title = place_displayer.display(self.db, self.source, fmt=0) + # get localized E,W translated to English + self.source.set_longitude(self.source.get_longitude().replace( + translate_en_loc['E'], 'E').replace(translate_en_loc['W'], 'W')) + # get localized N,S translated to English + self.source.set_latitude(self.source.get_latitude().replace( + translate_en_loc['N'], 'N').replace(translate_en_loc['S'], 'S')) if self.source.handle: # only commit if it has changed if self.source.serialize() != self.original: From f58f8a1e3d43f7fc0b3bfa8cd4ea49edbed933fe Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 25 Nov 2019 16:11:32 -0600 Subject: [PATCH 16/26] Fix empty PLAC/NAME when using private proxy --- gramps/gen/proxy/private.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gramps/gen/proxy/private.py b/gramps/gen/proxy/private.py index d53bdc09844..41a83cb95ae 100644 --- a/gramps/gen/proxy/private.py +++ b/gramps/gen/proxy/private.py @@ -1008,13 +1008,13 @@ def sanitize_place(db, place): n_name = PlaceName(name) n_name.citation_list = [] copy_citation_ref_list(db, name, n_name) - new_place.name_list.append(n_name) + new_place.add_name(n_name) # Copy type list for ptype in place.get_types(): n_type = PlaceType(ptype) n_type.citation_list = [] copy_citation_ref_list(db, ptype, n_type) - new_place.type_list.append(n_type) + new_place.add_type(n_type) # Copy placeref list for pref in place.placeref_list: n_pref = PlaceRef(pref) From ef4007d6e010de6e69e305e6c54f6b7659c0d5ba Mon Sep 17 00:00:00 2001 From: prculley Date: Tue, 26 Nov 2019 11:11:54 -0600 Subject: [PATCH 17/26] Fix Complete Individual report for crash when number of sections changes --- gramps/gui/plug/_guioptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gramps/gui/plug/_guioptions.py b/gramps/gui/plug/_guioptions.py index ba723f16663..7a42555c7dd 100644 --- a/gramps/gui/plug/_guioptions.py +++ b/gramps/gui/plug/_guioptions.py @@ -1858,6 +1858,12 @@ def __init__(self, option, dbstate, uistate, track, override): self.__cbutton = [] default = option.get_value().split(',') + # bug fix; if the report list of boolean options changes size, we get + # a failure, because stored report config values are wrong, so just + # reset values to all true in this case + if len(default) != len(option.get_descriptions()): + default = ['True'] * len(option.get_descriptions()) + option.set_value(','.join(default)) if len(default) < 15: columns = 2 # number of checkbox columns else: From 5336a73d0eafcd13faa43f9730a274f1aaa4b652 Mon Sep 17 00:00:00 2001 From: prculley Date: Wed, 4 Mar 2020 10:48:59 -0600 Subject: [PATCH 18/26] fix no_magic for dbapi --- gramps/plugins/db/dbapi/dbapi.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gramps/plugins/db/dbapi/dbapi.py b/gramps/plugins/db/dbapi/dbapi.py index e732cdd3fe7..f0f1b6260d7 100644 --- a/gramps/plugins/db/dbapi/dbapi.py +++ b/gramps/plugins/db/dbapi/dbapi.py @@ -252,9 +252,6 @@ def transaction_commit(self, txn): TXNUPD: "-update", TXNDEL: "-delete", None: "-delete"} - if txn.batch: - # FIXME: need a User GUI update callback here: - self.reindex_reference_map(lambda percent: percent) self.dbapi.commit() if not txn.batch: # Now, emit signals: @@ -620,8 +617,8 @@ def _commit_base(self, obj, obj_key, trans, change_time): [obj.handle, pickle.dumps(obj.serialize())]) self._update_secondary_values(obj) + self._update_backlinks(obj, trans) if not trans.batch: - self._update_backlinks(obj, trans) if old_data: trans.add(obj_key, TXNUPD, obj.handle, old_data, From 3f3780ee6884a25ce754492c61c5c79a953ce252 Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 12 Jun 2020 17:07:02 -0500 Subject: [PATCH 19/26] Fix exception bug in GrampsLoacle --- gramps/gen/utils/grampslocale.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gramps/gen/utils/grampslocale.py b/gramps/gen/utils/grampslocale.py index a3cd86de448..18f34180822 100644 --- a/gramps/gen/utils/grampslocale.py +++ b/gramps/gen/utils/grampslocale.py @@ -45,11 +45,11 @@ # logging.WARN. Uncomment the following to change it to logging.DEBUG: # LOG.setLevel(logging.DEBUG) try: - from icu import Locale, Collator + from icu import Locale, Collator, ICUError HAVE_ICU = True except ImportError: try: - from PyICU import Locale, Collator + from PyICU import Locale, Collator, ICUError HAVE_ICU = True except ImportError as err: # No logger, save the warning message for later. From ba78b1df65f3815a16db8e80dc0349772d750a8e Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 22 Jun 2020 16:13:18 -0500 Subject: [PATCH 20/26] Fix crash on change in number of columns in view --- gramps/gui/views/listview.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gramps/gui/views/listview.py b/gramps/gui/views/listview.py index fbe5f2fd05c..83c1b2cd890 100644 --- a/gramps/gui/views/listview.py +++ b/gramps/gui/views/listview.py @@ -524,6 +524,15 @@ def column_order(self): order = self._config.get('columns.rank') size = self._config.get('columns.size') vis = self._config.get('columns.visible') + # make sure an upgraded number of columns works + if len(order) != len(self.COLUMNS) or len(size) != len(self.COLUMNS): + # If not, reset everything to defaults + vis = self.CONFIGSETTINGS[0][1] + self._config.set('columns.visible', vis) + order = self.CONFIGSETTINGS[1][1] + self._config.set('columns.rank', order) + size = self.CONFIGSETTINGS[2][1] + self._config.set('columns.size', size) colord = [(1 if val in vis else 0, val, size) for val, size in zip(order, size)] From 091941e3c7dc4de4565b039af78160f0860ce69f Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 12 Jun 2020 17:27:24 -0500 Subject: [PATCH 21/26] GEPS045 Replace Place Types and Assign place groups menu commands --- gramps/gui/views/placetypes.py | 307 +++++++++++++++++++++++++++++ gramps/plugins/lib/libplaceview.py | 6 + 2 files changed, 313 insertions(+) create mode 100644 gramps/gui/views/placetypes.py diff --git a/gramps/gui/views/placetypes.py b/gramps/gui/views/placetypes.py new file mode 100644 index 00000000000..3f036c53a2f --- /dev/null +++ b/gramps/gui/views/placetypes.py @@ -0,0 +1,307 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2010 Nick Hall +# 2020 Paul Culley +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +""" +Provide PlaceType editing functionality. +""" +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +from gi.repository import Gtk + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext +from gramps.gen.lib import PlaceType +from gramps.gen.db import DbTxn +from gramps.gen.const import URL_MANUAL_PAGE +from ..display import display_help +import gramps.gui.widgets.progressdialog as progressdlg +from gramps.gui.widgets.placetypeselector import PlaceTypeSelector +from gramps.gui.widgets.monitoredwidgets import MonitoredDataType +from ..uimanager import ActionGroup +from ..managedwindow import ManagedWindow + +#------------------------------------------------------------------------- +# +# Constants +# +#------------------------------------------------------------------------- +PT_1 = ( + ''' + + + ''' + '''Place Types and Group + %s + + + ''') + +PT_2 = ( + ''' + + + ''' + '''Place Types and Group + %s + + + ''') + +PT_MENU = ( + ''' + win.ReplacePlaceType + ''' + '''Replace Place Type... + + + win.AssignPlaceGroup + ''' + '''Assign Place Group... + + ''') + +WIKI_HELP_PAGE = '%s_-_Filters' % URL_MANUAL_PAGE +WIKI_HELP_SEC2 = _('manual|Replace_PlaceType_dialog') +WIKI_HELP_SEC3 = _('manual|Assign_PlaceGroup_dialog') + + +def build_placetype_menu(uistate): + """ + Create the menu items for these commands. + """ + actions = [] + + actions.append(('ReplacePlaceType', ReplacePlaceType)) + actions.append(('AssignPlaceGroup', AssignPlaceGroup)) + + placetype_action = ActionGroup(name='PlaceType') + placetype_action.add_actions(actions) + uistate.uimanager.insert_action_group(placetype_action) + return [PT_1 % PT_MENU, PT_2 % PT_MENU] + + +#------------------------------------------------------------------------- +# +# ReplacePlaceType dialog +# +#------------------------------------------------------------------------- +class ReplacePlaceType(ManagedWindow): + """ + A dialog to enable the user to replace a place type. + """ + def __init__(self, dbstate, uistate, track): + view = uistate.viewmanager.active_page + self.selected = view.selected_handles() + if not self.selected: + return + self.db = dbstate.db + place = self.db.get_place_from_handle(self.selected[0]) + self.title = _('Replace Place Type') + ManagedWindow.__init__(self, uistate, track, self.__class__, + modal=True) + # the self.top.run() below makes Gtk make it modal, so any change to + # the previous line's "modal" would require that line to be changed + self.top = Gtk.Dialog(transient_for=self.parent_window) + self.top.vbox.set_spacing(5) + + grid = Gtk.Grid() + self.top.vbox.pack_start(grid, True, True, 10) + + # Original type + cbe_o = Gtk.ComboBox(has_entry=True) + self.o_ptype = place.get_placetype() + self.orig = PlaceTypeSelector(dbstate, cbe_o, self.o_ptype) + label = Gtk.Label(label=_('Original Place Type:')) + grid.attach(label, 0, 0, 1, 1) + grid.attach(cbe_o, 1, 0, 1, 1) + + # replacement type + cbe_n = Gtk.ComboBox(has_entry=True) + self.ptype_n = PlaceType('') + + self.sel = PlaceTypeSelector(dbstate, cbe_n, self.ptype_n) + label = Gtk.Label(label=_('Replacement Place Type:')) + grid.attach(label, 0, 1, 1, 1) + grid.attach(cbe_n, 1, 1, 1, 1) + + self.top.add_button(_('_Help'), Gtk.ResponseType.HELP) + self.top.add_button(_('_Cancel'), Gtk.ResponseType.CANCEL) + self.top.add_button(_('_OK'), Gtk.ResponseType.OK) + self.set_window(self.top, None, self.title) + self.show() + self.run() + + def build_menu_names(self, _obj): # this is meaningless while it's modal + return (self.title, None) + + def run(self): + """ + Run the dialog and return the result. + """ + while True: + # the self.top.run() makes Gtk make it modal, so any change to that + # line would require the ManagedWindow.__init__ to be changed also + response = self.top.run() + if response == Gtk.ResponseType.HELP: + display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC2) + else: + break + + if response == Gtk.ResponseType.OK: + self._save() + if response != Gtk.ResponseType.DELETE_EVENT: + self.close() + + def _save(self): + """ + start the process of replacing place types + """ + # Make the dialog modal so that the user can't start another + # database transaction while we are still running. + pmon = progressdlg.ProgressMonitor( + progressdlg.GtkProgressDialog, + ("", self.uistate.window, Gtk.DialogFlags.MODAL), + popup_time=2) + status = progressdlg.LongOpStatus(msg=_("Replacing place types"), + total_steps=len(self.selected), + interval=len(self.selected) // 20) + pmon.add_op(status) + msg = _('Replace Place Type: (%s)') % str(self.o_ptype) + with DbTxn(msg, self.db) as trans: + for handle in self.selected: + status.heartbeat() + place = self.db.get_place_from_handle(handle) + commit = False + for ptype in place.get_types(): + if ptype.is_same(self.o_ptype): + ptype.pt_id = self.ptype_n.pt_id + ptype.name = self.ptype_n.name + commit = True + if commit: + self.db.commit_place(place, trans) + status.end() + + +#------------------------------------------------------------------------- +# +# AssignPlaceGroup dialog +# +#------------------------------------------------------------------------- +class AssignPlaceGroup(ManagedWindow): + """ + A dialog to enable the user to assign a place group. + """ + def __init__(self, dbstate, uistate, track): + view = uistate.viewmanager.active_page + self.selected = view.selected_handles() + if not self.selected: + return + self.db = dbstate.db + self.place = self.db.get_place_from_handle(self.selected[0]) + self.title = _('Assign Place Group') + ManagedWindow.__init__(self, uistate, track, self.__class__, + modal=True) + # the self.top.run() below makes Gtk make it modal, so any change to + # the previous line's "modal" would require that line to be changed + self.top = Gtk.Dialog(transient_for=self.parent_window) + self.top.vbox.set_spacing(5) + + grid = Gtk.Grid() + self.top.vbox.pack_start(grid, True, True, 10) + + # Group + cbe = Gtk.ComboBox(has_entry=True) + + custom_placegroup_types = sorted(self.db.get_placegroup_types(), + key=lambda s: s.lower()) + self.place_group = MonitoredDataType(cbe, + self.place.set_group, + self.place.get_group, + custom_placegroup_types) + label = Gtk.Label(label=_('Place Group:')) + grid.attach(label, 0, 0, 1, 1) + grid.attach(cbe, 1, 0, 1, 1) + + self.top.add_button(_('_Help'), Gtk.ResponseType.HELP) + self.top.add_button(_('_Cancel'), Gtk.ResponseType.CANCEL) + self.top.add_button(_('_OK'), Gtk.ResponseType.OK) + self.set_window(self.top, None, self.title) + self.show() + self.run() + + def build_menu_names(self, _obj): # this is meaningless while it's modal + return (self.title, None) + + def run(self): + """ + Run the dialog and return the result. + """ + while True: + # the self.top.run() makes Gtk make it modal, so any change to that + # line would require the ManagedWindow.__init__ to be changed also + response = self.top.run() + if response == Gtk.ResponseType.HELP: + display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC3) + else: + break + + if response == Gtk.ResponseType.OK: + self._save() + if response != Gtk.ResponseType.DELETE_EVENT: + self.close() + + def _save(self): + """ + start the process of replacing place types + """ + # get group + group = self.place.group + + # Make the dialog modal so that the user can't start another + # database transaction while we are still running. + pmon = progressdlg.ProgressMonitor( + progressdlg.GtkProgressDialog, + ("", self.uistate.window, Gtk.DialogFlags.MODAL), + popup_time=2) + status = progressdlg.LongOpStatus(msg=_("Assigning place Groups"), + total_steps=len(self.selected), + interval=len(self.selected) // 20) + pmon.add_op(status) + msg = _('Assign Place Group: (%s)') % str(group) + with DbTxn(msg, self.db) as trans: + for handle in self.selected: + status.heartbeat() + place = self.db.get_place_from_handle(handle) + place.group = group + self.db.commit_place(place, trans) + status.end() diff --git a/gramps/plugins/lib/libplaceview.py b/gramps/plugins/lib/libplaceview.py index 43b3756a8fc..b2ab97e057a 100644 --- a/gramps/plugins/lib/libplaceview.py +++ b/gramps/plugins/lib/libplaceview.py @@ -41,6 +41,7 @@ from gramps.gui.views.listview import ListView, TEXT, ICON from gramps.gen.errors import WindowActiveError from gramps.gui.views.bookmarks import PlaceBookmarks +from gramps.gui.views.placetypes import build_placetype_menu from gramps.gen.config import config from gramps.gui.dialog import ErrorDialog from gramps.gui.pluginmanager import GuiPluginManager @@ -130,6 +131,7 @@ def __init__(self, pdata, dbstate, uistate, title, model, nav_group): uistate.connect('placeformat-changed', self.build_tree) _ui = self.__create_maps_menu_actions() + _ui.extend(build_placetype_menu(uistate)) self.additional_uis.append(_ui) def navigation_type(self): @@ -327,6 +329,8 @@ def get_stock(self): ''' '''Place Filter Editor + + ''', # Following are the Toolbar items ''' @@ -453,6 +457,8 @@ def get_stock(self): '''_Look up with Map Service
+ + ''' % _('action|_Edit...')] # to use sgettext() From 8e2a0f1673489e69d482e285696f29229dc83acc Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 12 Jun 2020 15:05:52 -0500 Subject: [PATCH 22/26] GEPS045 PlaceType plugins/addons --- .../PlaceTypes/locale/de/LC_MESSAGES/addon.mo | Bin 0 -> 819 bytes gramps/plugins/PlaceTypes/placetype_nl.gpr.py | 41 ++++++ gramps/plugins/PlaceTypes/placetype_nl.py | 127 ++++++++++++++++++ gramps/plugins/PlaceTypes/po/de-local.po | 54 ++++++++ gramps/plugins/PlaceTypes/po/template.pot | 46 +++++++ gramps/plugins/placetype_common.py | 124 +++++++++++++++++ gramps/plugins/placetype_custom.gpr.py | 44 ++++++ 7 files changed, 436 insertions(+) create mode 100644 gramps/plugins/PlaceTypes/locale/de/LC_MESSAGES/addon.mo create mode 100644 gramps/plugins/PlaceTypes/placetype_nl.gpr.py create mode 100644 gramps/plugins/PlaceTypes/placetype_nl.py create mode 100644 gramps/plugins/PlaceTypes/po/de-local.po create mode 100644 gramps/plugins/PlaceTypes/po/template.pot create mode 100644 gramps/plugins/placetype_common.py create mode 100644 gramps/plugins/placetype_custom.gpr.py diff --git a/gramps/plugins/PlaceTypes/locale/de/LC_MESSAGES/addon.mo b/gramps/plugins/PlaceTypes/locale/de/LC_MESSAGES/addon.mo new file mode 100644 index 0000000000000000000000000000000000000000..28a70efa35bdcdd07678447c8cb7235defb17a0d GIT binary patch literal 819 zcmYLH!EO^V5H-*OT7(c>xPUZVDiHFro0gJn8dN21qe@AMN+=w-IPoMgaqP(JjY!0A z^v01t;Q~j*g%jVxcvF#)p5DxOp6#*c?}M#NMs}O<8f%;U{62@arnx zzQ))+^6wG{?PKDg+aZpKE5w(C24Rch-`5$d68|CICcb}Tt$RouVjvFZ>=1|gn3$wx zj$C$=5RByIdN?y!(3aM+h4F65dd@DC#2f@@HE_Z$!H(bz{REvBM&{7dVu=0a9AP1J zfqBe~{?bx@8Ms>*HB@tyW zyZKm2-YmvB@7oNb-kUyeITW7i_>u6KK_#gqJlW@I3hAqioaAMaP$56Zg$n!quS;t@ zO?X;^bU)jxJttifbwuuY-wBgz;jPP{tK8H=2d$ZW|5oJa;9wR~hdQJLhEy!^H7U)w zpbmH+#Vmt1x|uL=FqOzvS~XnaA^EX&<4tYZTKBYjyy-QKlc;5lM??3Z2hG6aXa8cZ zg)(&*PK3+RH{SJ6cy&`3?mI%qkIm3ZWyTp)2g*kucpDA6bt4@+r5{)!q}4dB#Jf>X z7fxt?V%;pyz|6}qZ&d2Ab{kI(JZV7stR69HPO_##j~-ANgr*v3WoZDWFjF4R6w381 j;AR&1dvw%C%-(t-J!|8Pq>{dvo_4h!Dzjoo){WRd5^dR= literal 0 HcmV?d00001 diff --git a/gramps/plugins/PlaceTypes/placetype_nl.gpr.py b/gramps/plugins/PlaceTypes/placetype_nl.gpr.py new file mode 100644 index 00000000000..f3f24b07c29 --- /dev/null +++ b/gramps/plugins/PlaceTypes/placetype_nl.gpr.py @@ -0,0 +1,41 @@ +# encoding:utf-8 +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2020 Paul Culley +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + + +#------------------------------------------------------------------------ +# +# Common Placetypes +# +#------------------------------------------------------------------------ +register( + GENERAL, + category='PLACETYPES', + id='pt_nl', + name="Netherlands PlaceType values", + description=_("Provides a library of Netherlands PlaceType values."), + version='1.0', + status=STABLE, + fname='placetype_nl.py', + authors=["The Gramps project"], + authors_email=["http://gramps-project.org"], + load_on_reg=True, + gramps_target_version='5.1', +) diff --git a/gramps/plugins/PlaceTypes/placetype_nl.py b/gramps/plugins/PlaceTypes/placetype_nl.py new file mode 100644 index 00000000000..76f4ba70d89 --- /dev/null +++ b/gramps/plugins/PlaceTypes/placetype_nl.py @@ -0,0 +1,127 @@ +# encoding:utf-8 +# +# Gramps - a GTK+/GNOME based genealogy program - Records plugin +# +# Copyright (C) 2020 Paul Culley +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +#------------------------------------------------------------------------ +# +# Standard Python modules +# +#------------------------------------------------------------------------ +import logging +#------------------------------------------------------------------------ +# +# Gramps modules +# +#------------------------------------------------------------------------ +from gramps.gen.lib.placegrouptype import PlaceGroupType as P_G +from gramps.gen.lib.placetype import PlaceType +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext +LOG = logging.getLogger() + + +# _T_ is a gramps-defined keyword -- see po/update_po.py and po/genpot.sh +def _T_(value): # enable deferred translations (see Python docs 22.1.3.4) + return value + + +try: + _trans = glocale.get_addon_translator(__file__) +except ValueError: + _trans = glocale.translation +_ = _trans.sgettext + +_report_trans = None +_report_lang = None + + +def translate_func(ptype, locale=None, pt_id=None): + """ + This function provides translations for the locally defined place types. + It is called by the place type display code for the GUI and reports. + + The locale parameter is an instance of a GrampsLocale. This is used to + determine the language for tranlations. (locale.lang) It is also used + as a backup translation if no local po/mo file is present. + + :param ptype: the placetype translatable string + :type ptype: str + :param locale: the backup locale + :type locale: GrampsLocale instance + :returns: display string of the place type + :rtype: str + """ + global _report_lang, _report_trans + if locale is None or locale is glocale: + # using GUI language. + return _(ptype) + if locale.lang == _report_lang: + # We already created this locale, so use the previous version + # this will speed up reports in an alternate language + return _report_trans(ptype) + # We need to create a new language specific addon translator instance + try: + _r_trans = glocale.get_addon_translator( + __file__, languages=(locale.lang, )) + except ValueError: + _r_trans = glocale.translation + _report_trans = _r_trans.sgettext + _report_lang = locale.lang + return _report_trans(ptype) + + +# The data map (dict) contains a tuple with key as a handle and data as tuple; +# translatable name +# native name +# color (used for map markers, I suggest picking by probable group) +# probable group (used for legacy XML import and preloading Group in place +# editor) +# gettext method (or None if standard method) +DATAMAP = { + # add the common "Country" to the NL menu + "Country" : (_T_("Country"), "Country", + "#FFFF00000000", P_G(P_G.COUNTRY), None), + "nl_Province" : (_T_("nl|Province"), "Provincie", + "#0000FFFFFFFF", P_G(P_G.REGION), translate_func), + "nl_Municipality" : (_T_("nl|Municipality"), "Gemeente", + "#0000FFFFFFFF", P_G(P_G.REGION), translate_func), + "nl_City" : (_T_("nl|City"), "Stad", + "#0000FFFF0000", P_G(P_G.PLACE), translate_func), + "nl_Place" : (_T_("nl|Place"), "Plaats", + "#0000FFFF0000", P_G(P_G.PLACE), translate_func), + "nl_Village" : (_T_("nl|Village"), "Dorp", + "#0000FFFF0000", P_G(P_G.PLACE), translate_func), +} + + +def load_on_reg(_dbstate, _uistate, _plugin): + """ + Runs when plugin is registered. + """ + for hndl, tup in DATAMAP.items(): + # for these Netherlands elements, the category is 'NL' + # For larger regions of several countries, any descriptive text can be + # used (Holy Roman Empire) + # the register function returns True if the handle is a duplcate + # good idea to check this. + duplicate = PlaceType.register_placetype(hndl.capitalize(), tup, "NL") + if duplicate and hndl.startswith("nl_"): + LOG.debug("Duplicate handle %s detected; please fix", hndl) + PlaceType.update_name_map() diff --git a/gramps/plugins/PlaceTypes/po/de-local.po b/gramps/plugins/PlaceTypes/po/de-local.po new file mode 100644 index 00000000000..87f7b6d0820 --- /dev/null +++ b/gramps/plugins/PlaceTypes/po/de-local.po @@ -0,0 +1,54 @@ +# German translation for Gramps +# This file is distributed under the same license as the Gramps package. +# translation of de.po to Deutsch +# +# +# Anton Huber , 2005,2006. +# Sebastian Vöcking , 2005. +# Sebastian Vöcking , 2005. +# Martin Hawlisch , 2005, 2006. +# Alex Roitman , 2006. +# Mirko Leonhäuser , 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019. +# Alois Pöttker , 2017. +msgid "" +msgstr "" +"Project-Id-Version: de\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-11 17:06-0500\n" +"PO-Revision-Date: 2019-10-19 16:49+0200\n" +"Last-Translator: Mirko Leonhäuser \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 18.12.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: Placetypes/placetype_nl.gpr.py:33 +msgid "Provides a library of Netherlands PlaceType values." +msgstr "Bietet eine Bibliothek mit Niederlande PlaceType-Werten." + +#: Placetypes/placetype_nl.py:96 +msgid "Country" +msgstr "Land" + +#: Placetypes/placetype_nl.py:98 +msgid "nl|Province" +msgstr "Provinz" + +#: Placetypes/placetype_nl.py:100 +msgid "nl|Municipality" +msgstr "Gemeinde" + +#: Placetypes/placetype_nl.py:102 +msgid "nl|City" +msgstr "Stadt" + +#: Placetypes/placetype_nl.py:104 +msgid "nl|Place" +msgstr "Platz" + +#: Placetypes/placetype_nl.py:106 +msgid "nl|Village" +msgstr "Dorf" diff --git a/gramps/plugins/PlaceTypes/po/template.pot b/gramps/plugins/PlaceTypes/po/template.pot new file mode 100644 index 00000000000..f56c610fb4b --- /dev/null +++ b/gramps/plugins/PlaceTypes/po/template.pot @@ -0,0 +1,46 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-11 17:13-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: PlaceTypes/placetype_nl.gpr.py:33 +msgid "Provides a library of Netherlands PlaceType values." +msgstr "" + +#: PlaceTypes/placetype_nl.py:96 +msgid "Country" +msgstr "" + +#: PlaceTypes/placetype_nl.py:98 +msgid "nl|Province" +msgstr "" + +#: PlaceTypes/placetype_nl.py:100 +msgid "nl|Municipality" +msgstr "" + +#: PlaceTypes/placetype_nl.py:102 +msgid "nl|City" +msgstr "" + +#: PlaceTypes/placetype_nl.py:104 +msgid "nl|Place" +msgstr "" + +#: PlaceTypes/placetype_nl.py:106 +msgid "nl|Village" +msgstr "" diff --git a/gramps/plugins/placetype_common.py b/gramps/plugins/placetype_common.py new file mode 100644 index 00000000000..8b00ed4227f --- /dev/null +++ b/gramps/plugins/placetype_common.py @@ -0,0 +1,124 @@ +# encoding:utf-8 +# +# Gramps - a GTK+/GNOME based genealogy program - Records plugin +# +# Copyright (C) 2020 Paul Culley +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +#------------------------------------------------------------------------ +# +# Standard Python modules +# +#------------------------------------------------------------------------ +import datetime + +#------------------------------------------------------------------------ +# +# Gramps modules +# +#------------------------------------------------------------------------ +from gramps.gen.lib.placegrouptype import PlaceGroupType as P_G +from gramps.gen.lib.placetype import PlaceType +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext + + +# _T_ is a gramps-defined keyword -- see po/update_po.py and po/genpot.sh +def _T_(value): # enable deferred translations (see Python docs 22.1.3.4) + return value + + +COUNTRY = "Country" # 1 +STATE = "State" # 2 +COUNTY = "County" # 3 +CITY = "City" # 4 +PARISH = "Parish" # 5 +LOCALITY = "Locality" # 6 +STREET = "Street" # 7 +PROVINCE = "Province" # 8 +REGION = "Region" # 9 +DEPARTMENT = "Department" # 10 +NEIGHBORHOOD = "Neighborhood" # 11 +DISTRICT = "District" # 12 +BOROUGH = "Borough" # 13 +MUNICIPALITY = "Municipality" # 14 +TOWN = "Town" # 15 +VILLAGE = "Village" # 16 +HAMLET = "Hamlet" # 17 +FARM = "Farm" # 18 +BUILDING = "Building" # 19 +NUMBER = "Number" # 20 + +# The data map (dict) contains a tuple with key as a handle +# name +# native name +# countries +# color +# probable group (used for legacy XML import) +# gettext method (or None if standard method) +DATAMAP = { + COUNTRY : (_T_("Country"), "Country", + "#FFFF00000000", P_G(P_G.COUNTRY), None), + STATE : (_T_("State"), "State", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + COUNTY : (_T_("County"), "County", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + CITY : (_T_("City"), "City", + "#0000FFFF0000", P_G(P_G.PLACE), None), + PARISH : (_T_("Parish"), "Parish", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + LOCALITY : (_T_("Locality"), "Locality", + "#0000FFFF0000", P_G(P_G.PLACE), None), + STREET : (_T_("Street"), "Street", + "#0000FFFF0000", P_G(P_G.OTHER), None), + PROVINCE : (_T_("Province"), "Province", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + REGION : (_T_("Region"), "Region", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + DEPARTMENT : (_T_("Department"), "Department", + "#0000FFFFFFFF", P_G(P_G.REGION), None), + NEIGHBORHOOD : (_T_("Neighborhood"), "Neighborhood", + "#0000FFFF0000", P_G(P_G.PLACE), None), + DISTRICT : (_T_("District"), "District", + "#0000FFFF0000", P_G(P_G.PLACE), None), + BOROUGH : (_T_("Borough"), "Borough", + "#0000FFFF0000", P_G(P_G.PLACE), None), + MUNICIPALITY : (_T_("Municipality"), "Municipality", + "#0000FFFF0000", P_G(P_G.PLACE), None), + TOWN : (_T_("Town"), "Town", + "#0000FFFF0000", P_G(P_G.PLACE), None), + VILLAGE : (_T_("Village"), "Village", + "#0000FFFF0000", P_G(P_G.PLACE), None), + HAMLET : (_T_("Hamlet"), "Hamlet", + "#0000FFFF0000", P_G(P_G.PLACE), None), + FARM : (_T_("Farm"), "Farm", + "#0000FFFF0000", P_G(P_G.PLACE), None), + BUILDING : (_T_("Building"), "Building", + "#0000FFFF0000", P_G(P_G.BUILDING), None), + NUMBER : (_T_("Number"), "Number", + "#0000FFFF0000", P_G(P_G.OTHER), None), +} + + +def load_on_reg(_dbstate, _uistate, _plugin): + """ + Runs when plugin is registered. + """ + for hndl, tup in DATAMAP.items(): + # for these common elements, the category is '!!' + PlaceType.register_placetype(hndl, tup, "!!") + PlaceType.update_name_map() diff --git a/gramps/plugins/placetype_custom.gpr.py b/gramps/plugins/placetype_custom.gpr.py new file mode 100644 index 00000000000..d9330db0592 --- /dev/null +++ b/gramps/plugins/placetype_custom.gpr.py @@ -0,0 +1,44 @@ +# encoding:utf-8 +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2020 Paul Culley +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +from gramps.gen.plug._pluginreg import register, STABLE, GENERAL +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext + + +#------------------------------------------------------------------------ +# +# Common Placetypes +# +#------------------------------------------------------------------------ +register( + GENERAL, + category='PLACETYPES', + id='pt_common', + name="Common PlaceType values", + description=_("Provides a library of Common PlaceType values."), + version='1.0', + status=STABLE, + fname='placetype_common.py', + authors=["The Gramps project"], + authors_email=["http://gramps-project.org"], + load_on_reg=True, + gramps_target_version='5.1', +) From 03f976cce5f4b8a978eaefe63810046035f01d70 Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 15 Jun 2020 10:45:17 -0500 Subject: [PATCH 23/26] Fix Check&Repair to find bad handle references where handle is empty string --- gramps/plugins/tool/check.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gramps/plugins/tool/check.py b/gramps/plugins/tool/check.py index e4ec7415621..f6b79d62311 100644 --- a/gramps/plugins/tool/check.py +++ b/gramps/plugins/tool/check.py @@ -1546,7 +1546,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - person.replace_citation_references(None, new_handle) + person.replace_citation_references(item[1], new_handle) self.db.commit_person(person, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1560,7 +1560,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - family.replace_citation_references(None, new_handle) + family.replace_citation_references(item[1], new_handle) self.db.commit_family(family, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1574,7 +1574,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - place.replace_citation_references(None, new_handle) + place.replace_citation_references(item[1], new_handle) self.db.commit_place(place, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1588,7 +1588,8 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - citation.replace_citation_references(None, new_handle) + citation.replace_citation_references(item[1], + new_handle) self.db.commit_citation(citation, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1602,7 +1603,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - repository.replace_citation_references(None, + repository.replace_citation_references(item[1], new_handle) self.db.commit_repository(repository, self.trans) self.invalid_citation_references.add(new_handle) @@ -1617,7 +1618,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - obj.replace_citation_references(None, new_handle) + obj.replace_citation_references(item[1], new_handle) self.db.commit_media(obj, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1631,7 +1632,7 @@ def check_citation_references(self): if item[0] == 'Citation': if not item[1]: new_handle = create_id() - event.replace_citation_references(None, new_handle) + event.replace_citation_references(item[1], new_handle) self.db.commit_event(event, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: From 552b02e79f7c93203ee23c4477d0229056fd6481 Mon Sep 17 00:00:00 2001 From: prculley Date: Tue, 16 Jun 2020 10:06:08 -0500 Subject: [PATCH 24/26] Test Error handling --- gramps/test/test_util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gramps/test/test_util.py b/gramps/test/test_util.py index 300220ee953..c0200629800 100644 --- a/gramps/test/test_util.py +++ b/gramps/test/test_util.py @@ -242,10 +242,10 @@ def run(self, *args, stdin=None, bytesio=False): if handler.dbstate.is_open(): handler.dbstate.db.close() except: - print("Exception in test:") - print("-" * 60) - traceback.print_exc(file=sys.stdout) - print("-" * 60) + print("Exception in test:", file=sys.stderr) + print("-" * 60, file=sys.stderr) + traceback.print_exc(file=sys.stderr) + print("-" * 60, file=sys.stderr) return output From 27ce77171b05c7b5009c2c45eff17e24e4f12382 Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 3 Jul 2020 09:42:51 -0500 Subject: [PATCH 25/26] Fix Locations Gramplet to properly respond to signals --- gramps/plugins/gramplet/locations.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gramps/plugins/gramplet/locations.py b/gramps/plugins/gramplet/locations.py index fa2f9fa5dbb..2fbd44a190b 100644 --- a/gramps/plugins/gramplet/locations.py +++ b/gramps/plugins/gramplet/locations.py @@ -116,7 +116,10 @@ def main(self): if active_handle: active = self.dbstate.db.get_place_from_handle(active_handle) if active: - self.display_place(active, None, [active_handle], DateRange()) + visited = [active_handle] + self.display_place(active, None, visited, DateRange()) + self.callman.register_handles({'place': visited}) + else: self.set_has_data(False) else: @@ -179,12 +182,11 @@ def display_place(self, place, node, visited, drange): """ Display the location hierarchy for the active place. """ - self.callman.register_obj(place) for placeref in place.get_placeref_list(): if placeref.ref in visited: continue - dr2 = drange.intersect(placeref.date) + dr2 = drange.intersect(placeref.get_date_object()) if dr2.is_empty(): continue @@ -217,7 +219,6 @@ def display_place(self, place, node, visited, drange): """ Display the location hierarchy for the active place. """ - self.callman.register_obj(place) for link in self.dbstate.db.find_backlink_handles( place.handle, include_classes=['Place']): if link[1] in visited: @@ -228,11 +229,12 @@ def display_place(self, place, node, visited, drange): for placeref in child_place.get_placeref_list(): if placeref.ref == place.handle: - dr2 = drange.intersect(placeref.date) + dr2 = drange.intersect(placeref.get_date_object()) if dr2.is_empty(): continue self.add_place(placeref, child_place, node, visited, dr2) + self.callman.register_handles({'place': [child_place.handle]}) self.set_has_data(self.model.count > 0) self.model.tree.expand_all() From 8790fabadd63c5b548fa068f496639e606e667bd Mon Sep 17 00:00:00 2001 From: prculley Date: Fri, 17 Jul 2020 17:55:26 -0500 Subject: [PATCH 26/26] GEPS045 Fix Testcasegenerator for enhanced places --- gramps/plugins/tool/testcasegenerator.py | 270 +++++++++++++---------- 1 file changed, 150 insertions(+), 120 deletions(-) diff --git a/gramps/plugins/tool/testcasegenerator.py b/gramps/plugins/tool/testcasegenerator.py index 61efa208acb..ab5d01875d3 100644 --- a/gramps/plugins/tool/testcasegenerator.py +++ b/gramps/plugins/tool/testcasegenerator.py @@ -194,6 +194,29 @@ class TestcaseGenerator(tool.BatchTool): EventType.MARR_SETTL, EventType.CUSTOM]) + PLACETYPES = [ + "Unknown", # -1 original value + "Country", # 1 + "State", # 2 + "County", # 3 + "City", # 4 + "Parish", # 5 + "Locality", # 6 + "Street", # 7 + "Province", # 8 + "Region", # 9 + "Department", # 10 + "Neighborhood", # 11 + "District", # 12 + "Borough", # 13 + "Municipality", # 14 + "Town", # 15 + "Village", # 16 + "Hamlet", # 17 + "Farm", # 18 + "Building", # 19 + "Number"] # 20 + def __init__(self, dbstate, user, options_class, name, callback=None): uistate = user.uistate if uistate: @@ -390,33 +413,38 @@ def run_tool(self, cli=False): self.generate_data_errors(step) if self.options_dict['persons']: - with self.progress(_('Generating testcases'), - _('Generating families'), - self.max_person_count) \ - as self.progress_step: - self.person_count = 0 - - while True: - if not self.persons_todo: - pers_h = self.generate_person(0) - self.persons_todo.append(pers_h) - self.parents_todo.append(pers_h) - person_h = self.persons_todo.pop(0) - self.generate_family(person_h) - if _randint(0, 3) == 0: - self.generate_family(person_h) - if _randint(0, 7) == 0: - self.generate_family(person_h) - if self.person_count > self.max_person_count: - break - for child_h in self.parents_todo: - self.generate_parents(child_h) - if self.person_count > self.max_person_count: - break + self.generate_persons() if not cli: self.top.destroy() + def generate_persons(self): + """This creates the persons and families""" + with DbTxn(_("Testcase generator step %d") % self.transaction_count, + self.db) as self.trans, \ + self.progress(_('Generating testcases'), _('Generating families'), + self.max_person_count) \ + as self.progress_step: + self.person_count = 0 + + while True: + if not self.persons_todo: + pers_h = self.generate_person(0) + self.persons_todo.append(pers_h) + self.parents_todo.append(pers_h) + person_h = self.persons_todo.pop(0) + self.generate_family(person_h) + if _randint(0, 3) == 0: + self.generate_family(person_h) + if _randint(0, 7) == 0: + self.generate_family(person_h) + if self.person_count > self.max_person_count: + break + for child_h in self.parents_todo: + self.generate_parents(child_h) + if self.person_count > self.max_person_count: + break + def generate_data_errors(self, step): """This generates errors in the database to test src/plugins/tool/Check The module names correspond to the checking methods in @@ -1318,13 +1346,17 @@ def create_all_possible_citations(self, c_h_list, name, message): place = Place() place.set_title(message) + # Place place.add_citation(_choice(c_h_list)) + # Place : Name pname = PlaceName(value='All Attribute Test') pname.add_citation(_choice(c_h_list)) place.add_name(pname) - ptype = PlaceType(value=PlaceType.BOROUGH) + # Place : Type + ptype = PlaceType("Borough") ptype.add_citation(_choice(c_h_list)) place.add_type(ptype) + # Place : Attribute place.add_attribute(att) # Place : MediaRef mref = MediaRef() @@ -1337,7 +1369,8 @@ def create_all_possible_citations(self, c_h_list, name, message): att.add_citation(_choice(c_h_list)) mref.add_attribute(att) place.add_media_reference(mref) - place.add_attribute(att) + # Place : EventRef + place.add_event_ref(eref) self.db.add_place(place, self.trans) ref = Repository() @@ -1560,85 +1593,83 @@ def generate_family(self, person1_h): if person2_h and _randint(0, 2) > 0: self.parents_todo.append(person2_h) - with DbTxn(_("Testcase generator step %d") % self.transaction_count, - self.db) as self.trans: - self.transaction_count += 1 - fam = Family() - self.add_defaults(fam) - if person1_h: - fam.set_father_handle(person1_h) - if person2_h: - fam.set_mother_handle(person2_h) + self.transaction_count += 1 + fam = Family() + self.add_defaults(fam) + if person1_h: + fam.set_father_handle(person1_h) + if person2_h: + fam.set_mother_handle(person2_h) - # Avoid adding the same event more than once to the same family - event_set = set() + # Avoid adding the same event more than once to the same family + event_set = set() - # Generate at least one family event with a probability of 75% - if _randint(0, 3) > 0: - (dummy, eref) = self.rand_family_event(None) - fam.add_event_ref(eref) - event_set.add(eref.get_reference_handle()) + # Generate at least one family event with a probability of 75% + if _randint(0, 3) > 0: + (dummy, eref) = self.rand_family_event(None) + fam.add_event_ref(eref) + event_set.add(eref.get_reference_handle()) + + # generate some more events with a lower probability + while _randint(0, 3) == 1: + (dummy, eref) = self.rand_family_event(None) + if eref.get_reference_handle() in event_set: + continue + fam.add_event_ref(eref) + event_set.add(eref.get_reference_handle()) - # generate some more events with a lower probability - while _randint(0, 3) == 1: - (dummy, eref) = self.rand_family_event(None) - if eref.get_reference_handle() in event_set: - continue + # some shared events + if self.generated_events: + while _randint(0, 5) == 1: + typeval = EventType.UNKNOWN + while int(typeval) not in self.FAMILY_EVENTS: + e_h = _choice(self.generated_events) + typeval = self.db.get_event_from_handle(e_h).get_type() + if e_h in event_set: + break + eref = EventRef() + self.fill_object(eref) + eref.set_reference_handle(e_h) fam.add_event_ref(eref) - event_set.add(eref.get_reference_handle()) - - # some shared events - if self.generated_events: - while _randint(0, 5) == 1: - typeval = EventType.UNKNOWN - while int(typeval) not in self.FAMILY_EVENTS: - e_h = _choice(self.generated_events) - typeval = self.db.get_event_from_handle(e_h).get_type() - if e_h in event_set: - break - eref = EventRef() - self.fill_object(eref) - eref.set_reference_handle(e_h) - fam.add_event_ref(eref) - event_set.add(e_h) + event_set.add(e_h) - fam_h = self.db.add_family(fam, self.trans) - self.generated_families.append(fam_h) - fam = self.db.commit_family(fam, self.trans) - if person1_h: - person1 = self.db.get_person_from_handle(person1_h) - person1.add_family_handle(fam_h) - self.db.commit_person(person1, self.trans) - if person2_h: - person2 = self.db.get_person_from_handle(person2_h) - person2.add_family_handle(fam_h) - self.db.commit_person(person2, self.trans) + fam_h = self.db.add_family(fam, self.trans) + self.generated_families.append(fam_h) + fam = self.db.commit_family(fam, self.trans) + if person1_h: + person1 = self.db.get_person_from_handle(person1_h) + person1.add_family_handle(fam_h) + self.db.commit_person(person1, self.trans) + if person2_h: + person2 = self.db.get_person_from_handle(person2_h) + person2.add_family_handle(fam_h) + self.db.commit_person(person2, self.trans) - lastname = person1.get_primary_name().get_surname() + lastname = person1.get_primary_name().get_surname() - for i in range(0, _randint(1, 10)): - if self.person_count > self.max_person_count: - break - if alive_in_year: - child_h = self.generate_person( - None, lastname, - alive_in_year=alive_in_year + - _randint(16 + 2 * i, 30 + 2 * i)) - else: - child_h = self.generate_person(None, lastname) - (born, died) = self.person_dates[child_h] - alive_in_year = born - fam = self.db.get_family_from_handle(fam_h) - child_ref = ChildRef() - child_ref.set_reference_handle(child_h) - self.fill_object(child_ref) - fam.add_child_ref(child_ref) - self.db.commit_family(fam, self.trans) - child = self.db.get_person_from_handle(child_h) - child.add_parent_family_handle(fam_h) - self.db.commit_person(child, self.trans) - if _randint(0, 3) > 0: - self.persons_todo.append(child_h) + for i in range(0, _randint(1, 10)): + if self.person_count > self.max_person_count: + break + if alive_in_year: + child_h = self.generate_person( + None, lastname, + alive_in_year=alive_in_year + + _randint(16 + 2 * i, 30 + 2 * i)) + else: + child_h = self.generate_person(None, lastname) + (born, died) = self.person_dates[child_h] + alive_in_year = born + fam = self.db.get_family_from_handle(fam_h) + child_ref = ChildRef() + child_ref.set_reference_handle(child_h) + self.fill_object(child_ref) + fam.add_child_ref(child_ref) + self.db.commit_family(fam, self.trans) + child = self.db.get_person_from_handle(child_h) + child.add_parent_family_handle(fam_h) + self.db.commit_person(child, self.trans) + if _randint(0, 3) > 0: + self.persons_todo.append(child_h) def generate_parents(self, child_h): """ Add parents to a person, if not present already""" @@ -1666,28 +1697,26 @@ def generate_parents(self, child_h): if _randint(0, 2) > 1: self.parents_todo.append(person2_h) - with DbTxn(_("Testcase generator step %d") % self.transaction_count, - self.db) as self.trans: - self.transaction_count += 1 - fam = Family() - self.add_defaults(fam) - fam.set_father_handle(person1_h) - fam.set_mother_handle(person2_h) - child_ref = ChildRef() - child_ref.set_reference_handle(child_h) - self.fill_object(child_ref) - fam.add_child_ref(child_ref) - fam_h = self.db.add_family(fam, self.trans) - self.generated_families.append(fam_h) - fam = self.db.commit_family(fam, self.trans) - person1 = self.db.get_person_from_handle(person1_h) - person1.add_family_handle(fam_h) - self.db.commit_person(person1, self.trans) - person2 = self.db.get_person_from_handle(person2_h) - person2.add_family_handle(fam_h) - self.db.commit_person(person2, self.trans) - child.add_parent_family_handle(fam_h) - self.db.commit_person(child, self.trans) + self.transaction_count += 1 + fam = Family() + self.add_defaults(fam) + fam.set_father_handle(person1_h) + fam.set_mother_handle(person2_h) + child_ref = ChildRef() + child_ref.set_reference_handle(child_h) + self.fill_object(child_ref) + fam.add_child_ref(child_ref) + fam_h = self.db.add_family(fam, self.trans) + self.generated_families.append(fam_h) + fam = self.db.commit_family(fam, self.trans) + person1 = self.db.get_person_from_handle(person1_h) + person1.add_family_handle(fam_h) + self.db.commit_person(person1, self.trans) + person2 = self.db.get_person_from_handle(person2_h) + person2.add_family_handle(fam_h) + self.db.commit_person(person2, self.trans) + child.add_parent_family_handle(fam_h) + self.db.commit_person(child, self.trans) def generate_tags(self): """ Make up some odd tags """ @@ -2085,7 +2114,7 @@ def generate_place(self): # skip some levels in the place hierarchy continue place = Place() - place.set_type(PlaceType(type_num)) + place.set_type(self.PLACETYPES[type_num]) if parent_handle is not None: self.add_parent_place(place, parent_handle) if type_num > 1 and _randint(1, 3) == 1: @@ -2094,6 +2123,7 @@ def generate_place(self): if parent_handle is not None: self.add_parent_place(place, parent_handle) self.fill_object(place) + place.group = place.get_type().get_probable_group() self.db.add_place(place, self.trans) parent_handle = place.get_handle() self.generated_places.append(place.get_handle())