diff --git a/data/grampsxml.dtd b/data/grampsxml.dtd index ca3db021481..b77a0172580 100644 --- a/data/grampsxml.dtd +++ b/data/grampsxml.dtd @@ -24,15 +24,15 @@ --> @@ -58,10 +58,10 @@ DATABASE tags --> - - + + - - + False dialog @@ -77,7 +76,7 @@ False vertical - + True False @@ -88,7 +87,7 @@ False True - start + end _Title: True center @@ -96,31 +95,17 @@ 0 - 1 - - - - - True - False - start - True - Either use the two fields below to enter coordinates (latitude and longitude), - - - 0 - 3 - 5 + 0 - + True False - center + end L_atitude: True - center + right lat_entry @@ -129,42 +114,40 @@ - + True False start _Longitude: True center - lon_entry - 2 + 4 4 - True - True - Full title of this place. + True + True + Full title of this place. True - + 1 - 1 - 4 + 0 + 5 - + True False - start + end _ID: True - gid 0 @@ -174,37 +157,24 @@ True - True - Latitude (position above the Equator) of the place in decimal or degree notation. + 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 - + 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 @@ -222,9 +192,9 @@ You can set these values via the Geography View by searching the place, or via a - 0 + 1 5 - 4 + 5 @@ -235,51 +205,10 @@ You can set these values via the Geography View by searching the place, or via a True - True - Field used to paste info from a web page like Google, OpenStreetMap... + True + Field used to paste info from a web page like google, openstreetmap, ... True - - - - False - True - 1 - - - - - 0 - 6 - 4 - - - - - True - False - 6 - - - 200 - True - True - A unique ID to identify the place - - 15 - - - False - True - 0 - - - - - True - False - start - Code: - True + False @@ -287,24 +216,11 @@ You can set these values via the Geography View by searching the place, or via a 1 - - - True - True - Code associated with this place. Eg Country Code or Postal Code. - - 8 - - - True - True - 2 - - 1 - 7 + 6 + 5 @@ -312,6 +228,7 @@ You can set these values via the Geography View by searching the place, or via a True True True + end none @@ -333,8 +250,8 @@ You can set these values via the Geography View by searching the place, or via a - 4 - 2 + 6 + 0 @@ -342,9 +259,10 @@ You can set these values via the Geography View by searching the place, or via a True True True + end - 4 + 6 7 @@ -352,23 +270,23 @@ You can set these values via the Geography View by searching the place, or via a True False - start + end Name: 0 - 2 + 1 - + True False - start + end Type: - 2 + 0 2 @@ -376,72 +294,138 @@ You can set these values via the Geography View by searching the place, or via a True False - What type of place this is. Eg 'Country', 'City', ... + What type of place this is. Eg 'Country', 'City', ... . True 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. + 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 @@ -449,32 +433,141 @@ 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 + + @@ -495,6 +588,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/editplaceformat.glade b/gramps/gui/glade/editplaceformat.glade index 9d01d760f01..9e2ddb72195 100644 --- a/gramps/gui/glade/editplaceformat.glade +++ b/gramps/gui/glade/editplaceformat.glade @@ -6,6 +6,9 @@ False dialog + + + False @@ -15,9 +18,6 @@ False end - - - _Close @@ -93,13 +93,10 @@ 1 - - - False - True + False 1 @@ -122,23 +119,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 + 0 + 2 @@ -146,59 +199,388 @@ True False start - Language: + Formatting + + + - 0 - 3 + 0 + 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 - 3 + 0 + 6 + 2 - - Reverse display order + True - True - False + False + + + 0 + 3 + + + + + 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 + False + start + 6 + Where: + + + 0 + 1 + + + + + True + False + start + 6 + Type: + + + 0 + 3 + + + + + True + False + start True - True + All - 1 - 4 + 1 + 1 - + + All True - False + 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 + 1 + 2 + 3 - + + True + False + True + True + + + False + + + + + 1 + 3 + 3 + + + + True False start - Name: + 6 + Hierarchy: 0 @@ -206,29 +588,126 @@ - + True - True + False True + True + + + True + + - 1 - 0 + 1 + 0 + 3 - + True - True + False True + True + + + True + + - 1 - 1 + 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 + @@ -247,21 +726,23 @@ - True - True + False + True + 3 - True + False True - 1 + 0 - button1 + ok_btn + cancel_btn diff --git a/gramps/gui/glade/editplacename.glade b/gramps/gui/glade/editplacename.glade index 33936266139..0cec323b619 100644 --- a/gramps/gui/glade/editplacename.glade +++ b/gramps/gui/glade/editplacename.glade @@ -216,11 +216,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 303da53b3fe..945fcf77157 100644 --- a/gramps/gui/glade/editplaceref.glade +++ b/gramps/gui/glade/editplaceref.glade @@ -1,8 +1,7 @@ - - + False 600 @@ -20,6 +19,7 @@ _Help + False True True True @@ -36,6 +36,7 @@ _Cancel + False True True True @@ -51,6 +52,7 @@ _OK + False True True True @@ -76,8 +78,8 @@ True False start - 6 - 6 + 6 + 6 3 3 Reference information @@ -97,15 +99,14 @@ True True - True 6 - + True False 12 - 6 + 12 12 @@ -127,6 +128,8 @@ True True + Date range in which the enclosure is valid. + True 1 @@ -138,6 +141,8 @@ True True True + Invoke date editor + start True @@ -151,6 +156,49 @@ 0 + + + True + False + What type of hierarchy is this ('Administrative', 'Religious'), ... . + True + True + + + True + True + + + + + 4 + 0 + + + + + True + False + start + Hierarchy Type: + + + 3 + 0 + + + + + + + + + + + + + + @@ -195,75 +243,83 @@ True True - 6 + 20 True 6 True 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: - True - center + end + Name: 0 - 2 + 1 - + True - True + False start - True - Either use the two fields below to enter coordinates (latitude and longitude), + + + True + False + start + Either use the two fields below to enter coordinates (latitude and longitude), + + + False + True + 0 + + - 0 + 1 3 - 5 + 6 - + True False - start - 3 - 3 - ID: + end + _ID: True - center 0 @@ -271,12 +327,14 @@ - + True False - start - Latitude: + end + L_atitude: True + right + lat_entry 0 @@ -326,34 +384,32 @@ 0 8 - 5 + 7 - + True False - start + end Type: - True - center - 2 + 0 2 - + True False start - Longitude: + _Longitude: True center - 2 + 4 4 @@ -361,6 +417,7 @@ True False + start 6 @@ -378,9 +435,9 @@ - 0 + 1 5 - 4 + 5 @@ -404,28 +461,29 @@ - 0 + 1 6 - 4 + 5 True - True + False What type of place this is. Eg 'Country', 'City', ... - True True - + True + The type of this place . True - 3 + 1 2 + 2 @@ -433,6 +491,7 @@ True True True + end none @@ -454,8 +513,8 @@ - 4 - 4 + 6 + 0 @@ -468,15 +527,15 @@ 1 - 1 - 4 + 0 + 5 True True - Latitude (position above the Equator) of the place in decimal or degree notation. + 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 @@ -485,36 +544,47 @@ You can set these values via the Geography View by searching the place, or via a 1 4 + 3 - + True - True - Longitude (position relative to the Prime, or Greenwich, Meridian) of the place in decimal or degree notation. + False + + + 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 - + True + + + + False + True + 0 + + - 3 + 5 4 - + True False 6 - 200 True True A unique ID to identify the place - 15 + 18 False @@ -523,12 +593,11 @@ You can set these values via the Geography View by searching the place, or via a - + True False start - Code: - True + Tags: False @@ -537,15 +606,14 @@ You can set these values via the Geography View by searching the place, or via a - + True - True - Code associated with this place. Eg Country Code or Postal Code. - - 8 + False + start + True - True + False True 2 @@ -554,30 +622,7 @@ You can set these values via the Geography View by searching the place, or via a 1 7 - - - - - True - False - start - Tags: - - - 2 - 7 - - - - - True - False - start - True - - - 3 - 7 + 5 @@ -585,9 +630,10 @@ You can set these values via the Geography View by searching the place, or via a True True True + end - 4 + 6 7 @@ -608,49 +654,143 @@ You can set these values via the Geography View by searching the place, or via a - + True - False + True + The name of this place. + True + + + + 1 + 1 + 5 + + + + + True + True + True + Invoke place name editor. + end - + True - True - The name of this place. - True - + False + gtk-edit - - False - True - 0 - + + + 6 + 1 + + + + + 37 + True + True + True + Invoke place type editor. + start - + True + False + gtk-edit + + + + + 3 + 2 + + + + + True + False + What type of place this is. Eg 'Country', 'City', ... . + True + + True - True - - - True - False - Invoke place name editor. - gtk-edit - - + The type of this place . + True - - False - True - 1 - - 1 + 5 + 2 + + + + + True + False + start + Category: + + + 4 2 + + + 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 + + @@ -661,9 +801,6 @@ You can set these values via the Geography View by searching the place, or via a - - False - diff --git a/gramps/gui/glade/editplacetype.glade b/gramps/gui/glade/editplacetype.glade new file mode 100644 index 00000000000..da99e9180c0 --- /dev/null +++ b/gramps/gui/glade/editplacetype.glade @@ -0,0 +1,231 @@ + + + + + + + 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 place this is. Eg 'Country', 'City', ... . + True + + + True + The type of this place . + 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 75421b245a0..2c17ad271f6 100644 --- a/gramps/gui/glade/mergeplace.glade +++ b/gramps/gui/glade/mergeplace.glade @@ -5,7 +5,10 @@ False True - dialog + dialog + + + True @@ -243,8 +246,8 @@ primary data for the merged place. True - 0 - 5 + 0 + 4 @@ -259,8 +262,8 @@ primary data for the merged place. lat_btn1 - 2 - 5 + 2 + 4 @@ -274,8 +277,8 @@ primary data for the merged place. True - 0 - 6 + 0 + 5 @@ -290,8 +293,8 @@ primary data for the merged place. long_btn1 - 2 - 6 + 2 + 5 @@ -305,8 +308,8 @@ primary data for the merged place. True - 0 - 7 + 0 + 6 @@ -321,8 +324,8 @@ primary data for the merged place. gramps_btn1 - 2 - 7 + 2 + 6 @@ -357,8 +360,8 @@ primary data for the merged place. False - 1 - 5 + 1 + 4 @@ -369,8 +372,8 @@ primary data for the merged place. False - 3 - 5 + 3 + 4 @@ -381,8 +384,8 @@ primary data for the merged place. False - 1 - 6 + 1 + 5 @@ -393,8 +396,8 @@ primary data for the merged place. False - 3 - 6 + 3 + 5 @@ -405,8 +408,8 @@ primary data for the merged place. False - 1 - 7 + 1 + 6 @@ -417,8 +420,8 @@ primary data for the merged place. False - 3 - 7 + 3 + 6 @@ -541,65 +544,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 abf5307a105..b3be735750d 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, callback=None): 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,10 +145,10 @@ def __init__(self, dbstate, uistate, track, handle1, handle2, callback=None): 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) + title1 + " [" + gramps1 + "] " + str(self.pl1.get_type()) ) rbutton_label2.set_label( - title2 + " [" + gramps2 + "] " + str(self.pl2.place_type) + title2 + " [" + gramps2 + "] " + str(self.pl2.get_type()) ) rbutton1.connect("toggled", self.on_handle1_toggled) @@ -171,7 +163,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) @@ -179,7 +170,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) @@ -207,8 +197,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/plug/_guioptions.py b/gramps/gui/plug/_guioptions.py index 472f4541b21..938b97d3710 100644 --- a/gramps/gui/plug/_guioptions.py +++ b/gramps/gui/plug/_guioptions.py @@ -1881,6 +1881,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: diff --git a/gramps/gui/selectors/selectplace.py b/gramps/gui/selectors/selectplace.py index 5159342d38d..1b848bdaffb 100644 --- a/gramps/gui/selectors/selectplace.py +++ b/gramps/gui/selectors/selectplace.py @@ -69,7 +69,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): @@ -78,10 +78,10 @@ def get_from_handle_func(self): def setup_searches(self): """Build the default searches and add them to the search bar. This overrides the baseselector method because we use the hidden - COL_SEARCH (11) that has alt names as well as primary name for name + COL_SEARCH (10) that has alt names as well as primary name for name searching""" cols = [ - (pair[3], pair[1] if pair[1] else 11, pair[0] in self.exact_search()) + (pair[3], 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/listview.py b/gramps/gui/views/listview.py index 5e6a3e1b6ec..ce3cdeba7ea 100644 --- a/gramps/gui/views/listview.py +++ b/gramps/gui/views/listview.py @@ -577,6 +577,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)] return colord diff --git a/gramps/gui/views/placetypes.py b/gramps/gui/views/placetypes.py new file mode 100644 index 00000000000..57536314589 --- /dev/null +++ b/gramps/gui/views/placetypes.py @@ -0,0 +1,260 @@ +# 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 + + +WIKI_HELP_PAGE = "%s_-_Filters" % URL_MANUAL_PAGE +WIKI_HELP_SEC2 = _("manual|Replace_PlaceType_dialog") +WIKI_HELP_SEC3 = _("manual|Assign_PlaceGroup_dialog") + + +# ------------------------------------------------------------------------- +# +# ReplacePlaceType dialog +# +# ------------------------------------------------------------------------- +class ReplacePlaceType(ManagedWindow): + """ + A dialog to enable the user to replace a place type. + """ + + def __init__(self, dbstate, uistate): + 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, [], 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_type() + 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): + 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, [], 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/gui/views/tags.py b/gramps/gui/views/tags.py index a5562943294..91bdc68611b 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 # ------------------------------------------------------------------------- # diff --git a/gramps/gui/views/treemodels/placemodel.py b/gramps/gui/views/treemodels/placemodel.py index 4b7786d5ecc..4555cddaed1 100644 --- a/gramps/gui/views/treemodels/placemodel.py +++ b/gramps/gui/views/treemodels/placemodel.py @@ -46,7 +46,7 @@ # Gramps modules # # ------------------------------------------------------------------------- -from gramps.gen.lib import Place, PlaceType +from gramps.gen.lib import Place, PlaceType, PlaceGroupType from gramps.gen.lib.json_utils import data_to_object from gramps.gen.datehandler import format_time from gramps.gen.utils.place import conv_lat_lon, coord_formats @@ -79,12 +79,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, ] @@ -93,12 +93,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, ] @@ -134,11 +134,11 @@ def column_title(self, data): def column_name(self, data): """Return the primary name""" - return data.name.value + return data.name_list[0].value def search_name(self, data): - """The search name includes all alt names to enable finding by alt name""" - return ",".join([data.name.value] + [name.value for name in data.alt_names]) + """The search name includes all names""" + return ",".join([name.value for name in data.name_list]) def column_longitude(self, data): if not data.long: @@ -184,10 +184,7 @@ def column_id(self, data): return data.gramps_id def column_type(self, data): - return PlaceType.get_str(data.place_type) - - def column_code(self, data): - return data.code + return PlaceType.str(data.type_list[0]) def column_private(self, data): if data.private: @@ -202,6 +199,9 @@ def sort_change(self, data): def column_change(self, data): return format_time(data.change) + def column_group(self, data): + return str(PlaceGroupType(data.group)) + def get_tag_name(self, tag_handle): """ Return the tag name from the given tag handle. diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index 7e87a3f7be1..2dc84d8853c 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -44,3 +44,4 @@ from .validatedmaskedentry import * from .placewithin import * from .persistenttreeview import * +from .placetypeselector import * diff --git a/gramps/gui/widgets/monitoredwidgets.py b/gramps/gui/widgets/monitoredwidgets.py index fccac966b00..1ab38436c22 100644 --- a/gramps/gui/widgets/monitoredwidgets.py +++ b/gramps/gui/widgets/monitoredwidgets.py @@ -82,6 +82,7 @@ # # ------------------------------------------------------------------------- class MonitoredCheckbox: + def __init__(self, obj, button, set_val, get_val, on_toggle=None, readonly=False): self.button = button self.button.connect("toggled", self._on_toggle) @@ -113,6 +114,7 @@ def _on_toggle(self, obj): # # ------------------------------------------------------------------------- class MonitoredEntry: + def __init__( self, obj, set_val, get_val, read_only=False, autolist=None, changed=None ): @@ -370,6 +372,7 @@ def on_change(self, obj): # # ------------------------------------------------------------------------- class MonitoredType: + def __init__( self, obj, set_val, get_val, mapping, custom, readonly=False, custom_values=None ): @@ -419,6 +422,7 @@ def on_change(self, obj): # # ------------------------------------------------------------------------- class MonitoredDataType: + def __init__( self, obj, @@ -427,6 +431,7 @@ def __init__( readonly=False, custom_values=None, ignore_values=None, + changed=None, ): """ Constructor for the MonitoredDataType class. @@ -446,11 +451,14 @@ def __init__( :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() @@ -507,6 +515,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) # ------------------------------------------------------------------------- @@ -515,6 +525,7 @@ def on_change(self, obj): # # ------------------------------------------------------------------------- class MonitoredMenu: + def __init__(self, obj, set_val, get_val, mapping, readonly=False, changed=None): self.set_val = set_val self.get_val = get_val diff --git a/gramps/gui/widgets/placetypeselector.py b/gramps/gui/widgets/placetypeselector.py new file mode 100644 index 00000000000..768c03bf9a2 --- /dev/null +++ b/gramps/gui/widgets/placetypeselector.py @@ -0,0 +1,266 @@ +# +# 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 + self.store = Gtk.TreeStore( # the menu store (pt_id, name, sensitive) + str, str, bool + ) + self.lstore = Gtk.ListStore(str, str) # a completion list (pt_id, name) + + self.ignore = False + self.fill_models() # fill out the initial models + self.combo.set_model(self.store) + combo.set_entry_text_column(1) + for cell in self.combo.get_cells(): + self.combo.add_attribute(cell, "sensitive", 2) + # fill out completion + self.e_completion = Gtk.EntryCompletion() + self.e_completion.set_model(self.lstore) + 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) + self.set_value(ptype) + + combo.connect("changed", self.on_combo_change) + if not self.sidebar: + self.combo.set_sensitive(not self.dbstate.db.readonly) + + 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. + """ + # fill completion store and get menu + menu = self.get_menu(self.dbstate.db) + # 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. + for heading, items in menu: + if not heading: # add ptype in items if expand + parent = None + else: + parent = self.store.append(None, row=["", _(heading), False]) + for item in items: + if not isinstance(item[1], list): + self.store.append(parent, row=[item[0], item[1], True]) + continue + else: + heading_2, items_2 = item + parent_2 = self.store.append(parent, row=["", heading_2, False]) + for item_2 in items_2: + self.store.append(parent_2, row=[item_2[0], item_2[1], True]) + return + + def update_models(self): + """Update the models with the latest menu data.""" + self.store.clear() + self.lstore.clear() + self.fill_models() + + def set_value(self, val): + """ + Set value according to given place type. + + :param val: (int,str) tuple with the values to set. + :type val: tuple + """ + self.store.foreach(self.set_model_position, val.pt_id, val.str(expand=True)) + self.combo.get_child().set_text(val.str(expand=True)) + + def set_model_position(self, model, path, node, pt_id, name): + if model.get_value(node, 0) == pt_id and model.get_value(node, 1) == name: + self.combo.set_active_iter(node) + return True + return False + + def on_combo_change(self, combo): + """Deal with changes in the combo or entry; put results in the + PlaceType + """ + if self.ignore: + return + 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 + txt = combo.get_child().get_text().strip() + # if txt: + self.ptype.set((PlaceType.CUSTOM, txt)) + 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.ignore = True + self.set_value(self.ptype) + self.ignore = False + + def get_menu(self, db): + """This creates a place type menu structure suitable for + PlaceTypeSelector. + 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 PlaceTypeSelector. + + The completion store is also filled with the DATAMAP and db data. + + :returns: A menu suitable for PlaceTypeSelector. + :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: + self.tstore.append(row=[ptype.pt_id, name]) + types[ptype.pt_id] = ptype + + menu = [] # the whole menu + 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())) + cats.update(ptype.get_countries().split()) + for ptype in db.get_place_types(): + # they are all CUSTOM + types.append((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)) + # fill in completion store as well + self.lstore.append(row=[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 menu 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 00000000000..28a70efa35b Binary files /dev/null and b/gramps/plugins/PlaceTypes/locale/de/LC_MESSAGES/addon.mo differ diff --git a/gramps/plugins/PlaceTypes/placetype_nl.gpr.py b/gramps/plugins/PlaceTypes/placetype_nl.gpr.py new file mode 100644 index 00000000000..bf80f59ad68 --- /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="6.0", +) diff --git a/gramps/plugins/PlaceTypes/placetype_nl.py b/gramps/plugins/PlaceTypes/placetype_nl.py new file mode 100644 index 00000000000..23c92ebcf71 --- /dev/null +++ b/gramps/plugins/PlaceTypes/placetype_nl.py @@ -0,0 +1,152 @@ +# 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, context=""): # enable deferred translations + return "%s\x04%s" % (context, value) if context else 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_("Province", "nl"), + "Provincie", + "#0000FFFFFFFF", + P_G(P_G.REGION), + translate_func, + ), + "nl_Municipality": ( + _T_("Municipality", "nl"), + "Gemeente", + "#0000FFFFFFFF", + P_G(P_G.REGION), + translate_func, + ), + "nl_City": ( + _T_("City", "nl"), + "Stad", + "#0000FFFF0000", + P_G(P_G.PLACE), + translate_func, + ), + "nl_Place": ( + _T_("Place", "nl"), + "Plaats", + "#0000FFFF0000", + P_G(P_G.PLACE), + translate_func, + ), + "nl_Village": ( + _T_("Village", "nl"), + "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/drawreport/statisticschart.py b/gramps/plugins/drawreport/statisticschart.py index 6e9ebac21ff..89226d410b8 100644 --- a/gramps/plugins/drawreport/statisticschart.py +++ b/gramps/plugins/drawreport/statisticschart.py @@ -462,6 +462,7 @@ def __init__(self): self.get_event_type, ), } + self._place_format = None # ----------------- data extraction methods -------------------- # take an object and return a list of strings @@ -541,7 +542,7 @@ 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")] @@ -554,7 +555,7 @@ 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: @@ -869,6 +870,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) @@ -1192,6 +1194,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/export/export.gpr.py b/gramps/plugins/export/export.gpr.py index 1ec8b2982c4..f084ee34343 100644 --- a/gramps/plugins/export/export.gpr.py +++ b/gramps/plugins/export/export.gpr.py @@ -90,7 +90,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/exportcsv.py b/gramps/plugins/export/exportcsv.py index 6f92b01eeea..43d45f7365e 100644 --- a/gramps/plugins/export/exportcsv.py +++ b/gramps/plugins/export/exportcsv.py @@ -54,13 +54,12 @@ # # ------------------------------------------------------------------------- from gramps.gen.config import config -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.string import gender as gender_map from gramps.gen.datehandler import get_date from gramps.gen.display.place import displayer as _pd -from gramps.gui.glade import Glade from gramps.gen.constfunc import win @@ -331,17 +330,21 @@ 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 = "" + for att in place.get_attribute_list(): + if att.get_type() == AttributeType.POSTAL: + place_code = att.get_value() + break if place.placeref_list: for placeref in place.placeref_list: 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 @@ -370,7 +373,7 @@ def export_data(self): ) self.update() self.writeln() - ########################### sort: + # sort: sortorder = [] dropped_surnames = set() for key in self.plist: diff --git a/gramps/plugins/export/exportgedcom.py b/gramps/plugins/export/exportgedcom.py index 587328ba807..321f53e2c35 100644 --- a/gramps/plugins/export/exportgedcom.py +++ b/gramps/plugins/export/exportgedcom.py @@ -9,6 +9,7 @@ # Copyright (C) 2010 Nick Hall # Copyright (C) 2011 Tim G L Lyons # Copyright (C) 2012 Doug Blank +# Copyright (C) 2017 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 @@ -34,6 +35,7 @@ # ------------------------------------------------------------------------- import os import time +import re # ------------------------------------------------------------------------- # @@ -53,6 +55,7 @@ LdsOrd, NameType, PlaceType, + PlaceHierType, NoteType, Person, UrlType, @@ -66,8 +69,11 @@ 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.win32locale import _LOCALE_NAMES +from gramps.gen.const import GRAMPS_LOCALE as glocale + +_ = glocale.translation.gettext # ------------------------------------------------------------------------- # @@ -116,29 +122,6 @@ LdsOrd.STATUS_UNCLEARED: "UNCLEARED", } -LANGUAGES = { - "cs": "Czech", - "da": "Danish", - "nl": "Dutch", - "en": "English", - "eo": "Esperanto", - "fi": "Finnish", - "fr": "French", - "de": "German", - "hu": "Hungarian", - "it": "Italian", - "lt": "Latvian", - "lv": "Lithuanian", - "no": "Norwegian", - "po": "Polish", - "pt": "Portuguese", - "ro": "Romanian", - "sk": "Slovak", - "es": "Spanish", - "sv": "Swedish", - "ru": "Russian", -} - # ------------------------------------------------------------------------- # # @@ -239,6 +222,45 @@ 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 @@ -257,6 +279,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): @@ -268,6 +291,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): """ @@ -281,13 +305,21 @@ 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 + 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 + total_steps = ( + person_len + family_len + source_len + repo_len + note_len + place_len + ) self.set_total(total_steps) self._header(filename) self._submitter() self._individuals() self._families() + if self.include_ext_places: + self._places() self._sources() self._repos() self._notes() @@ -391,9 +423,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): """ @@ -1110,6 +1142,176 @@ def _note_record(self, note): if 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. @@ -1585,11 +1787,35 @@ 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: @@ -1598,6 +1824,15 @@ def _place(self, place, dateobj, level): self._writeln(level + 1, "MAP") self._writeln(level + 2, "LATI", latitude) self._writeln(level + 2, "LONG", longitude) + 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): diff --git a/gramps/plugins/export/exportgeneweb.py b/gramps/plugins/export/exportgeneweb.py index 4e0b8f6ee9c..530803f7c6d 100644 --- a/gramps/plugins/export/exportgeneweb.py +++ b/gramps/plugins/export/exportgeneweb.py @@ -279,7 +279,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 = "" @@ -293,7 +293,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 != "": @@ -378,14 +378,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 cdb56bf5e57..70613140287 100644 --- a/gramps/plugins/export/exportvcalendar.py +++ b/gramps/plugins/export/exportvcalendar.py @@ -214,7 +214,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/export/exportxml.py b/gramps/plugins/export/exportxml.py index 9444bcaa3fb..66e63ce93d6 100644 --- a/gramps/plugins/export/exportxml.py +++ b/gramps/plugins/export/exportxml.py @@ -59,7 +59,7 @@ _ = 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 @@ -806,29 +806,73 @@ 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) - self.write_date(date, index + 1) - self.g.write("%s\n" % sp) + 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.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: @@ -855,6 +899,7 @@ def write_event(self, event, index=1): self.g.write("%s\n" % sp) def dump_ordinance(self, ord, index=1): + name = ord.type2xml() sp = " " * index @@ -1332,20 +1377,19 @@ 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.dump_place_name(place.get_name(), index + 1) - for pname in place.get_alternative_names(): + 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: @@ -1356,7 +1400,7 @@ def write_place_obj(self, place, 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_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) @@ -1364,6 +1408,11 @@ def write_place_obj(self, place, index=1): for tag_handle in place.get_tag_list(): 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)) def write_object(self, obj, index=1): diff --git a/gramps/plugins/gramplet/attributes.py b/gramps/plugins/gramplet/attributes.py index d880c8c6f79..6cc22bb5109 100644 --- a/gramps/plugins/gramplet/attributes.py +++ b/gramps/plugins/gramplet/attributes.py @@ -270,3 +270,33 @@ 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/citations.py b/gramps/plugins/gramplet/citations.py index e5691ba7880..880b82dac5b 100644 --- a/gramps/plugins/gramplet/citations.py +++ b/gramps/plugins/gramplet/citations.py @@ -157,7 +157,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(): @@ -269,8 +276,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): @@ -561,6 +579,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() @@ -572,6 +592,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 diff --git a/gramps/plugins/gramplet/events.py b/gramps/plugins/gramplet/events.py index 22837af8c5c..7d8ea69a79b 100644 --- a/gramps/plugins/gramplet/events.py +++ b/gramps/plugins/gramplet/events.py @@ -60,7 +60,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): @@ -329,3 +329,83 @@ def get_start_date(self): active = self.db.get_family_from_handle(active_handle) 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 = PersistentTreeView(self.uistate, __name__) + 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 71fb6c9d4b5..3ea76d60866 100644 --- a/gramps/plugins/gramplet/gramplet.gpr.py +++ b/gramps/plugins/gramplet/gramplet.gpr.py @@ -755,6 +755,22 @@ help_url=GRAMPLET_HELP, ) +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"], + help_url=GRAMPLET_HELP, +) + register( GRAMPLET, id="Person Notes", @@ -1553,3 +1569,19 @@ navtypes=["Family"], help_url=GRAMPLET_HELP, ) + +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"], + help_url=GRAMPLET_HELP, +) diff --git a/gramps/plugins/gramplet/locations.py b/gramps/plugins/gramplet/locations.py index 0ab01c94d09..ef605f56a65 100644 --- a/gramps/plugins/gramplet/locations.py +++ b/gramps/plugins/gramplet/locations.py @@ -86,10 +86,11 @@ def build_gui(self): 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"), 6, 225), + (_("ID"), 4, 75), + (_("Hierarchy"), 5, 100), ("", NOSORT, 50), ] self.model = ListModel( @@ -121,7 +122,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: @@ -139,6 +143,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() @@ -147,7 +152,15 @@ def add_place(self, placeref, place, node, visited, drange): place_date += " *" new_node = self.model.add( - [place.handle, place_name, place_type, place_date, place_id, place_sort], + [ + place.handle, + place_name, + place_type, + place_date, + place_id, + place_hier, + place_sort, + ], node=node, ) @@ -181,12 +194,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 @@ -221,7 +233,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"] ): @@ -237,6 +248,7 @@ def display_place(self, place, node, visited, drange): 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() diff --git a/gramps/plugins/gramplet/placedetails.py b/gramps/plugins/gramplet/placedetails.py index 1aa6d3c2852..2b8fc46da1e 100644 --- a/gramps/plugins/gramplet/placedetails.py +++ b/gramps/plugins/gramplet/placedetails.py @@ -69,7 +69,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. """ @@ -83,6 +83,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): """ @@ -95,10 +99,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) @@ -128,10 +131,31 @@ def display_place(self, place): ) 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(), @@ -143,21 +167,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/graph/gvfamilylines.py b/gramps/plugins/graph/gvfamilylines.py index c3b1fa68a88..02d8ceb0efb 100644 --- a/gramps/plugins/graph/gvfamilylines.py +++ b/gramps/plugins/graph/gvfamilylines.py @@ -205,6 +205,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) @@ -430,6 +432,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") @@ -1183,7 +1186,9 @@ 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 567b2411ed3..60537b40ccc 100644 --- a/gramps/plugins/graph/gvrelgraph.py +++ b/gramps/plugins/graph/gvrelgraph.py @@ -152,6 +152,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") @@ -841,7 +842,7 @@ 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 "" @@ -932,6 +933,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/importer/importcsv.py b/gramps/plugins/importer/importcsv.py index 69786762223..2b12a9797fd 100644 --- a/gramps/plugins/importer/importcsv.py +++ b/gramps/plugins/importer/importcsv.py @@ -43,7 +43,6 @@ import time from io import TextIOWrapper -from gramps.gen.config import config # ------------------------------------------------------------------------- # @@ -53,10 +52,12 @@ from gramps.gen.const import GRAMPS_LOCALE as glocale from gramps.gen.datehandler import parser as _dp from gramps.gen.db import DbTxn +from gramps.gen.config import config from gramps.gen.display.place import displayer as place_displayer from gramps.gen.errors import GrampsImportError as Error from gramps.gen.lib import ( Attribute, + AttributeType, ChildRef, Citation, Event, @@ -181,18 +182,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.eventref = {} - 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")), "firstname": ( @@ -564,7 +553,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) @@ -1079,15 +1068,19 @@ def _parse_place(self, line_number, row, col): if tag is not None: self.add_tag(place, tag) 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: @@ -1110,16 +1103,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: @@ -1269,12 +1256,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/importgedcom.py b/gramps/plugins/importer/importgedcom.py index c468ced5720..6eebbb3254e 100644 --- a/gramps/plugins/importer/importgedcom.py +++ b/gramps/plugins/importer/importgedcom.py @@ -53,6 +53,7 @@ import importlib importlib.reload(module) +import time from gramps.gen.config import config @@ -113,9 +114,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: @@ -167,4 +169,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/importer/importxml.py b/gramps/plugins/importer/importxml.py index 9be810fdcf2..c346255e8d6 100644 --- a/gramps/plugins/importer/importxml.py +++ b/gramps/plugins/importer/importxml.py @@ -75,6 +75,10 @@ Person, PersonRef, Place, + PlaceAbbrev, + PlaceAbbrevType, + PlaceGroupType, + PlaceHierType, PlaceName, PlaceRef, PlaceType, @@ -92,6 +96,7 @@ Url, ) from gramps.gen.lib.json_utils import data_to_object +from gramps.gen.lib.placetype import DM_NAME from gramps.gen.db import DbTxn # from gramps.gen.db.write import CLASS_TO_KEY_MAP @@ -597,8 +602,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) @@ -748,6 +753,9 @@ 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), @@ -1280,14 +1288,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", "") @@ -1327,21 +1337,50 @@ def start_location(self, attrs): ) 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)) + ptype = PlaceType(typ) + self.placeobj.set_type(ptype) + self.placeobj.group = ptype.get_probable_group() + if attrs.get("postal"): + attr = Attribute() + attr.set_type(AttributeType.POSTAL) + attr.set_value(attrs.get("postal")) + self.placeobj.add_attribute(attr) + if attrs.get("phone"): + 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 @@ -1472,6 +1511,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): """ @@ -1480,7 +1521,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() @@ -1503,6 +1548,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() @@ -1864,7 +1911,7 @@ 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): @@ -1872,11 +1919,55 @@ def start_place_name(self, attrs): 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 + + def start_place_type(self, attrs): + """added at 1.8.0 place type list with date, type, citation + Note: place.group is added at 1.8.0 as well and set elsewhere.""" + 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: @@ -2231,6 +2322,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: @@ -2546,6 +2643,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("-") @@ -2643,6 +2742,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"] @@ -2759,6 +2860,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() @@ -2828,16 +2931,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 @@ -2915,9 +3021,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/importer/test/importvcard_test.py b/gramps/plugins/importer/test/importvcard_test.py index 977e887a463..82083ea2e29 100644 --- a/gramps/plugins/importer/test/importvcard_test.py +++ b/gramps/plugins/importer/test/importvcard_test.py @@ -70,6 +70,7 @@ def setUp(self): def canonicalize(self, doc): handles = {} + people = None for element in doc.iter("*"): gramps_id = element.get("id") if gramps_id is not None: @@ -80,10 +81,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") @@ -120,10 +139,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/libgedcom.py b/gramps/plugins/lib/libgedcom.py index f146c1a4348..e572e2e4316 100644 --- a/gramps/plugins/lib/libgedcom.py +++ b/gramps/plugins/lib/libgedcom.py @@ -90,13 +90,15 @@ # # ------------------------------------------------------------------------- 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 @@ -143,6 +145,13 @@ Person, PersonRef, Place, + PlaceAbbrev, + PlaceAbbrevType, + PlaceName, + PlaceHierType, + PlaceRef, + PlaceType, + PlaceGroupType as P_G, RepoRef, Repository, RepositoryType, @@ -154,9 +163,6 @@ Tag, Url, UrlType, - PlaceType, - PlaceRef, - PlaceName, ) from gramps.gen.lib.json_utils import data_to_object, object_to_dict from gramps.gen.db import DbTxn @@ -167,12 +173,11 @@ 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.const import DIFFERENT 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 +from gramps.gen.datehandler import displayer +from gramps.gen.utils.grampslocale import GrampsLocale, _LOCALE_NAMES # ------------------------------------------------------------------------- # @@ -312,11 +317,25 @@ TOKEN__JUST = 135 TOKEN__TEXT = 136 TOKEN__DATE = 137 -TOKEN__APID = 138 -TOKEN__CALLNAME = 139 +TOKEN_ADR3 = 138 +TOKEN__APID = 139 +TOKEN__CALLNAME = 140 +TOKEN__GOV = 141 +TOKEN__POST = 142 +TOKEN__FPOST = 143 +TOKEN__MAIDENHEAD = 144 +TOKEN__FSTAE = 145 +TOKEN__FCTRY = 146 +TOKEN__NAMC = 147 +TOKEN__DMGD = 148 +TOKEN__AIDN = 149 +TOKEN_FONE = 150 +TOKEN_ROMN = 151 +TOKEN__GOVTYPE = 152 TOKENS = { "_ADPN": TOKEN__ADPN, + "_AIDN": TOKEN__AIDN, # place ADMINISTRATIVE_IDENTIFIER "_AKA": TOKEN__AKA, "_AKAN": TOKEN__AKA, "_ALIA": TOKEN_ALIA, @@ -328,17 +347,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, @@ -346,9 +372,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, @@ -376,6 +404,7 @@ "ADOPT": TOKEN_ADOP, "ADR1": TOKEN_ADR1, "ADR2": TOKEN_ADR2, + "ADR3": TOKEN_ADR3, "AFN": TOKEN_AFN, "AGE": TOKEN_AGE, "AGENCY": TOKEN_IGNORE, @@ -440,6 +469,7 @@ "FAMS": TOKEN_FAMS, "FAX": TOKEN_FAX, "FILE": TOKEN_FILE, + "FONE": TOKEN_FONE, # Phonetic variation "FORM": TOKEN_FORM, "GEDC": TOKEN_GEDC, "GEDCOM": TOKEN_GEDC, @@ -497,6 +527,7 @@ "RFN": TOKEN_RFN, "RIN": TOKEN_RIN, "ROLE": TOKEN_ROLE, + "ROMN": TOKEN_ROMN, # ROMANIZED_VARIATION "SCHEMA": TOKEN__SCHEMA, "SEX": TOKEN_SEX, "SLGC": TOKEN_SLGC, @@ -1901,7 +1932,7 @@ 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. """ @@ -1919,13 +1950,16 @@ 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 = 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 + 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 @@ -1937,7 +1971,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 @@ -1955,105 +1988,6 @@ def __setattr__(self, name, value): self.__dict__[name] = value -# ------------------------------------------------------------------------- -# -# PlaceParser -# -# ------------------------------------------------------------------------- -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. - """ - - __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, - } - - def __init__(self, line=None): - self.parse_function = [] - - 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 - """ - 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 - - 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 - - # ------------------------------------------------------------------------- # # IdFinder @@ -2169,7 +2103,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. @@ -2214,6 +2148,19 @@ def __parse_name_personal(text): name.set_first_name(text.strip()) return name + @staticmethod + 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.set_type(PlaceHierType.ADMIN) + for ref in place.placeref_list: + if ref.is_equivalent(pref) != DIFFERENT: + return + place.add_placeref(pref) + def __init__( self, dbase, @@ -2240,8 +2187,11 @@ def __init__( 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 = {} @@ -2300,16 +2250,20 @@ def __init__( 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) + # 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 + self.fid2id = {} # Family + self.sid2id = {} # Source + self.oid2id = {} # Media + self.rid2id = {} # Repo + self.nid2id = {} # Note + self.lid2id = {} # Place + self.locs_list = [] # # Parse table for <> below the level 0 SUBM tag @@ -2470,6 +2424,8 @@ def __init__( 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 @@ -2684,25 +2640,6 @@ def __init__( } 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 # @@ -2872,13 +2809,14 @@ def __init__( 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, @@ -2896,25 +2834,134 @@ def __init__( 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} + 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.__event_place_object, - TOKEN_SOUR: self.__event_place_sour, - TOKEN__LOC: self.__ignore, - TOKEN_MAP: self.__place_map, + TOKEN_OBJE: self.__place_object, + TOKEN_SOUR: self.__place_sour, # 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) 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) + # 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, @@ -3033,7 +3080,7 @@ def __init__( 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) @@ -3057,6 +3104,8 @@ def __init__( 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) @@ -3120,12 +3169,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.place_import.generate_hierarchy(self.trans) - + 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: @@ -3159,25 +3208,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): """ @@ -3190,7 +3239,7 @@ def __find_or_create_person(self, gramps_id): person = data_to_object(self.dbase.get_raw_person_data(intid)) else: person = Person() - 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 @@ -3206,7 +3255,7 @@ def __find_or_create_family(self, gramps_id): family = data_to_object(self.dbase.get_raw_family_data(intid)) else: family = Family() - 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) # Add a counter for reordering the children later: @@ -3224,7 +3273,7 @@ def __find_or_create_media(self, gramps_id): obj = data_to_object(self.dbase.get_raw_media_data(intid)) else: obj = Media() - 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 @@ -3242,7 +3291,7 @@ def __find_or_create_source(self, gramps_id): obj = data_to_object(self.dbase.get_raw_source_data(intid)) else: obj = Source() - 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 @@ -3261,7 +3310,7 @@ def __find_or_create_repository(self, gramps_id): repository = data_to_object(self.dbase.get_raw_repository_data(intid)) else: repository = Repository() - 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 @@ -3285,60 +3334,141 @@ def __find_or_create_note(self, gramps_id): note = data_to_object(self.dbase.get_raw_note_data(intid)) else: note = Note() - 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: 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. + """ + 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, + } + + def __parse_form(self, line): """ - Determines whether a location is empty. + Provide the ability to parse GEDCOM PLAC.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. - @param location: The current location - @type location: gen.lib.Location - @return True of False + It does this my mapping the text strings (separated by commas) to the + corresponding PlaceType. """ - if location is None: - return True - elif object_to_dict(location) == self._EMPTY_LOC: - return True - elif location.is_empty(): - return True - return False + 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, location, placeref_list): + def __find_place(self, title, ptype, pref, gov=None, xref=None, no_find=None): """ - Finds an existing place based on the title and primary location. + 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 location: The current location - @type location: gen.lib.Location + @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: - 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: - return place + 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): """ - 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 @@ -3346,36 +3476,265 @@ 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(), + # we have a place + ptypes = ( + [] + if (self.loc_loc_mode and sub_state.place.gramps_id) + else sub_state.place_pf ) - 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()) + 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 + title = "" + for indx in reversed(range(len(places))): + name = places[indx].strip() + if not name: + continue + 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, gov=gov, xref=xref + ) + if place is not None: + 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.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) + # 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: + # still making hierarchy, Create the place + place = Place() + else: + # a new place with all the items + place = sub_state.place + if gov: + place.gramps_id = gov + place.set_title(title) + 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: - place.merge(sub_state.place) - place_title = _pd.display(self.dbase, place) - location = sub_state.pf.load_place( - self.place_import, place, place_title + # No hierarchy available, just save it. + 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, ) - self.dbase.commit_place(place, self.trans) - if location: - self.place_import.store_location(location, place.handle) - event.set_place_handle(place.get_handle()) + 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: + 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 + 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 + 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: + # 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.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()) + 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 __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 @@ -3583,17 +3942,20 @@ def _backup(self): self.backoff = True 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_from_handle(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.dbase.method("commit_%s", objtype), self.trans, db=self.dbase, ) @@ -3611,8 +3973,9 @@ def __check(_map, has_gid_func, class_func, commit_func, gramps_id2handle, msg): gramps_id, self.explanation.handle, class_func, - commit_func, + self.dbase.method("commit_%s", objtype), self.trans, + db=self.dbase, ) self.missing_references += 1 self.__add_msg( @@ -3629,52 +3992,25 @@ def __check(_map, has_gid_func, class_func, commit_func, gramps_id2handle, msg): self.missing_references = 0 __check( - self.pid_map, - self.dbase.has_person_gramps_id, - self.__find_or_create_person, - self.dbase.commit_person, - self.gid2id, - "INDI", - ) - __check( - self.fid_map, - self.dbase.has_family_gramps_id, - self.__find_or_create_family, - self.dbase.commit_family, - self.fid2id, - "FAM", + self.pid_map, "person", self.__find_or_create_person, self.gid2id, "INDI" ) __check( - self.sid_map, - self.dbase.has_source_gramps_id, - self.__find_or_create_source, - self.dbase.commit_source, - self.sid2id, - "SOUR", + self.fid_map, "family", self.__find_or_create_family, self.fid2id, "FAM" ) __check( - self.oid_map, - self.dbase.has_media_gramps_id, - self.__find_or_create_media, - self.dbase.commit_media, - self.oid2id, - "OBJE", + self.sid_map, "source", self.__find_or_create_source, self.sid2id, "SOUR" ) + __check(self.oid_map, "media", self.__find_or_create_media, self.oid2id, "OBJE") __check( self.rid_map, - self.dbase.has_repository_gramps_id, + "repository", self.__find_or_create_repository, - self.dbase.commit_repository, self.rid2id, "REPO", ) + __check(self.nid_map, "note", self.__find_or_create_note, self.nid2id, "NOTE") __check( - self.nid_map, - self.dbase.has_note_gramps_id, - self.__find_or_create_note, - self.dbase.commit_note, - self.nid2id, - "NOTE", + self.lid_map, "place", self.__find_or_create_place, self.lid2id, "PLACE" ) # Check persons membership in referenced families @@ -3684,7 +4020,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) @@ -3717,7 +4053,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() @@ -3794,13 +4130,14 @@ def __input_pid(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} @@ -3809,12 +4146,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 @@ -3827,54 +4163,69 @@ 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() + 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(), ): - 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 + 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): """ @@ -3971,6 +4322,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 @@ -4007,6 +4360,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 ", "SOURCE ")): # A source formatted in a single line, for example: # 0 @S62@ SOUR This is the title of the source @@ -4186,6 +4541,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): @@ -4393,12 +4749,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.pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4515,7 +4870,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): """ @@ -4539,7 +4894,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() @@ -4547,7 +4901,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): @@ -4643,12 +4997,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.pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -4666,7 +5019,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): @@ -5041,23 +5394,16 @@ 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 = None - sub_state.place_fields = 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): """ @@ -5105,7 +5451,7 @@ def __lds_form(self, line, state): @param state: The current state @type state: CurrentState """ - state.pf = PlaceParser(line) + state.place_pf = self.__parse_form(line) def __lds_plac(self, line, state): """ @@ -5116,21 +5462,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.strip() + if state.place is None: + state.place = Place() + state.place.set_title(title) + state.place.set_name(PlaceName(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): """ @@ -5445,26 +5789,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) @@ -5523,12 +5847,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.pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5580,12 +5903,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.pf = self.place_parser self.__parse_level(sub_state, self.event_parse_tbl, self.__undefined) state.msg += sub_state.msg @@ -5664,23 +5986,17 @@ 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 - sub_state.place_fields = 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): """ @@ -5894,14 +6210,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( + value = mimetypes_types_map.get( "." + sub_state.form, _("unknown") ) photo.set_mime_type(value) @@ -6105,21 +6421,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.set_name(PlaceName(value=line.data)) sub_state = CurrentState() sub_state.place = place @@ -6127,12 +6438,14 @@ 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 + state.place_gov = sub_state.place_gov # 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 @@ -6141,16 +6454,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 = self.__parse_form(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 @@ -6159,7 +6472,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 @@ -6168,9 +6481,560 @@ def __event_place_sour(self, line, state): """ state.place.add_citation(self.handle_source(line, state.level, state)) - def __place_map(self, line, 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 n+1 LONG n+1 LATI @@ -6211,98 +7075,73 @@ 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() + ptype = PlaceType("Address") + state.addr_pf = [ptype] + state.addr = [title] + sub_state.place.set_name(PlaceName(value=title)) + sub_state.place.group = P_G(P_G.PLACE) 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 + # structured address provided + + 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: - # 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) + return PlaceType(typ) - 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] + 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.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_note_list(addr.note_list) + sub_state.place.set_citation_list(addr.citation_list) + sub_state.place.get_name().date = addr.date + state.addr_place = sub_state.place def __event_privacy(self, line, state): """ @@ -6650,33 +7489,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): """ @@ -6722,6 +7553,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 @@ -6731,7 +7571,11 @@ 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): """ @@ -6742,7 +7586,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): """ @@ -7215,14 +8063,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, _("unknown")) + value = mimetypes_types_map.get("." + state.form, _("unknown")) state.media.set_mime_type(value) # Add the default reference if no source has found self.__add_default_source(media) @@ -7490,15 +8338,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): @@ -7551,106 +8397,124 @@ 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): + def __optional_note(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) + self.__parse_note(line, state.obj, state) - 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) + # ---------------------------------------------------------------------- + # + # _LOC Location parsing + # + # ---------------------------------------------------------------------- - 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 __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 - 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) + state = CurrentState() + state.place = place + state.level = 1 + state.event = Event() + self.__parse_level(state, self._loc_tbl, self.__ignore) - 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) + 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: - # 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 - @type line: GedLine - @param state: The current state - @type state: CurrentState - """ - self.__parse_note(line, state.obj, state) + # 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)) # ---------------------------------------------------------------------- # @@ -8001,14 +8865,14 @@ 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 @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): """ @@ -8246,7 +9110,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) @@ -8360,12 +9224,11 @@ 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.pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) if ( @@ -8394,12 +9257,11 @@ def __build_family_event_pair(self, state, event_type, event_map, description): 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.pf = self.place_parser self.__parse_level(sub_state, event_map, self.__undefined) state.msg += sub_state.msg @@ -8507,17 +9369,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. @@ -8583,13 +9443,21 @@ 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): """ @@ -8605,7 +9473,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 ) @@ -8622,7 +9490,7 @@ def __detect_file_decoder(self, input_file): else: input_file.seek(0) return TextIOWrapper( - input_file, encoding="utf-8", errors="replace", newline=None + input_file, encoding="utf-8", errors="surrogateescape", newline=None ) def parse(self): @@ -8632,6 +9500,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 @@ -8653,15 +9523,48 @@ 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 self.__is_xref_value(value): + 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)) @@ -8683,14 +9586,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): """ @@ -8704,6 +9607,18 @@ 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 + # ------------------------------------------------------------------------- # @@ -8749,6 +9664,39 @@ 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. diff --git a/gramps/plugins/lib/libgrampsxml.py b/gramps/plugins/lib/libgrampsxml.py index a93e6aa5643..2f1db97927d 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, 2) # version for Gramps 6.0 +GRAMPS_XML_VERSION_TUPLE = (1, 9, 0) # version for Gramps 6.0 places1 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 719d659beaa..6830be35a79 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 @@ -98,10 +101,14 @@ 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): """ @@ -110,12 +117,15 @@ 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 cb281113d49..9212a00be54 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 ReplacePlaceType, AssignPlaceGroup from gramps.gen.config import config from gramps.gui.dialog import ErrorDialog from gramps.gui.pluginmanager import GuiPluginManager @@ -74,12 +75,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 = [ @@ -87,16 +88,17 @@ 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.visible", [COL_NAME, COL_ID, COL_TYPE]), ( "columns.rank", [ @@ -104,7 +106,7 @@ class PlaceBaseView(ListView): COL_TITLE, COL_ID, COL_TYPE, - COL_CODE, + COL_GROUP, COL_LAT, COL_LON, COL_PRIV, @@ -112,7 +114,7 @@ class PlaceBaseView(ListView): 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") @@ -150,6 +152,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(self.build_placetype_menu()) self.additional_uis.append(_ui) def navigation_type(self): @@ -358,6 +361,8 @@ def get_stock(self): """ """Place Filter Editor + + """, # Following are the Toolbar items """ @@ -487,6 +492,8 @@ def get_stock(self): """_Look up with Map Service + + """ % _("_Edit...", "action"), @@ -648,10 +655,85 @@ def get_default_gramplets(self): "Place Gallery", "Place Citations", "Place Notes", + "Place Attributes", + "Place Events", "Place Backlinks", ), ) + # ------------------------------------------------------------------------- + # + # PlaceType menu + # + # ------------------------------------------------------------------------- + + def build_placetype_menu(self): + """ + Create the menu items for these commands. + """ + 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... + + """ + ) + actions = [] + + actions.append( + ( + "ReplacePlaceType", + self.doReplacePlaceType, + ) + ) + actions.append( + ( + "AssignPlaceGroup", + self.doAssignPlaceGroup, + ) + ) + + placetype_action = ActionGroup(name="PlaceType") + placetype_action.add_actions(actions) + self.uistate.uimanager.insert_action_group(placetype_action) + return [PT_1 % PT_MENU, PT_2 % PT_MENU] + + def doReplacePlaceType(self, *_dummy): + ReplacePlaceType(self.dbstate, self.uistate) + + def doAssignPlaceGroup(self, *_dummy): + AssignPlaceGroup(self.dbstate, self.uistate) + def make_callback(func, val): return lambda x, y: func(val) diff --git a/gramps/plugins/lib/libprogen.py b/gramps/plugins/lib/libprogen.py index 001bac74a74..e43c6560475 100644 --- a/gramps/plugins/lib/libprogen.py +++ b/gramps/plugins/lib/libprogen.py @@ -81,6 +81,7 @@ Person, Place, PlaceName, + PlaceType, Source, SrcAttribute, Surname, @@ -909,6 +910,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/lib/libsubstkeyword.py b/gramps/plugins/lib/libsubstkeyword.py index 0599ef75db3..09f616c95eb 100644 --- a/gramps/plugins/lib/libsubstkeyword.py +++ b/gramps/plugins/lib/libsubstkeyword.py @@ -40,9 +40,9 @@ # Gramps modules # # ------------------------------------------------------------------------ -from gramps.gen.lib import EventType, PlaceType, Location +from gramps.gen.lib import EventType, 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 @@ -355,37 +355,70 @@ 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, place.event_date) + 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, @@ -393,13 +426,18 @@ def parse_format(self, database, place): 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, + 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) @@ -1474,7 +1512,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 "") main_loc.set_locality("Second district" if 1 in y_or_n else "") main_loc.set_city("Arco" if 2 in y_or_n else "") diff --git a/gramps/plugins/lib/maps/geography.py b/gramps/plugins/lib/maps/geography.py index 648dc42d6ae..510498c9750 100644 --- a/gramps/plugins/lib/maps/geography.py +++ b/gramps/plugins/lib/maps/geography.py @@ -23,7 +23,6 @@ """ This module is the base class for all geography view module """ - # ------------------------------------------------------------------------- # # Python modules @@ -410,6 +409,7 @@ def build_nav_menu(self, osm, event, lat, lon): title = _("Lock zoom and position") add_item = Gtk.MenuItem(label=title) add_item.connect("activate", self.config_zoom_and_position, event, lat, lon) + add_item.show() menu.append(add_item) add_item = Gtk.MenuItem(label=_("Add place")) @@ -957,6 +957,16 @@ def printview(self, *obj): Print or save the view that is currently shown """ dummy_obj = obj + if Gtk.MAJOR_VERSION == 3 and Gtk.MINOR_VERSION < 11: + from gramps.gui.dialog import WarningDialog + + WarningDialog( + _("You can't use the print functionality"), + _("Your Gtk version is too old."), + parent=self.uistate.window, + ) + return + req = self.osm.get_allocation() widthpx = req.width heightpx = req.height @@ -1134,7 +1144,7 @@ def init_place(place): 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)) @@ -1178,15 +1188,14 @@ def init_place(place): 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): """ @@ -1280,7 +1289,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 @@ -1291,24 +1301,29 @@ def __add_place(self, parent, plat, plon): value = self.select_fct.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 = self.select_fct.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) @@ -1343,6 +1358,9 @@ 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) @@ -1509,10 +1527,11 @@ def choosen_map(self, *obj): self.change_map(self.osm, config.get("geography.map_service")) self.reload_tiles() return - # if map_source != config.get("geography.personal-map"): - # config.set("geography.map_service", constants.PERSONAL) - # self.change_new_map(name, map_source) - # self.reload_tiles() + + # if map_source != config.get("geography.personal-map"): + # config.set("geography.map_service", constants.PERSONAL) + # self.change_new_map(name, map_source) + # self.reload_tiles() def set_tilepath(self, *obj): """ diff --git a/gramps/plugins/lib/maps/markerlayer.py b/gramps/plugins/lib/maps/markerlayer.py index 0147bf788a7..a1c1a2a39af 100644 --- a/gramps/plugins/lib/maps/markerlayer.py +++ b/gramps/plugins/lib/maps/markerlayer.py @@ -26,7 +26,6 @@ # # ------------------------------------------------------------------------- import time -import logging from math import pi as PI from gi.repository import GObject from gi.repository import Gdk @@ -37,6 +36,8 @@ # Set up logging # # ------------------------------------------------------------------------ +import logging + _LOG = logging.getLogger("maps.markerlayer") # ------------------------------------------------------------------------- diff --git a/gramps/plugins/lib/maps/placeselection.py b/gramps/plugins/lib/maps/placeselection.py index ad0f933e0a7..66959e197d1 100644 --- a/gramps/plugins/lib/maps/placeselection.py +++ b/gramps/plugins/lib/maps/placeselection.py @@ -41,7 +41,7 @@ from gramps.gui.managedwindow import ManagedWindow from gramps.gui.dialog import WarningDialog 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 from .osmgps import OsmGps @@ -213,9 +213,9 @@ def slider_change(self, obj, lat, lon): 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, ""), + PLACE_STRING % loc.get("Country", ""), + PLACE_STRING % loc.get("State", ""), + PLACE_STRING % loc.get("County", ""), PLACE_STRING % _("Other"), self.oldvalue, ) @@ -267,25 +267,27 @@ 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 parent_list: 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 e7734f246f8..94584622668 100644 --- a/gramps/plugins/mapservices/eniroswedenmap.py +++ b/gramps/plugins/mapservices/eniroswedenmap.py @@ -76,9 +76,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() @@ -97,7 +97,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: @@ -109,8 +109,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: @@ -140,7 +140,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 != "") diff --git a/gramps/plugins/placetype_common.py b/gramps/plugins/placetype_common.py new file mode 100644 index 00000000000..04b985a041a --- /dev/null +++ b/gramps/plugins/placetype_common.py @@ -0,0 +1,123 @@ +# 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..d55e13476ee --- /dev/null +++ b/gramps/plugins/placetype_custom.gpr.py @@ -0,0 +1,45 @@ +# 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="6.0", +) diff --git a/gramps/plugins/test/exports_test.py b/gramps/plugins/test/exports_test.py index 4f8a193c3b2..e06495d7e4e 100644 --- a/gramps/plugins/test/exports_test.py +++ b/gramps/plugins/test/exports_test.py @@ -27,13 +27,20 @@ import tempfile from gramps.test.test_util import Gramps -from gramps.gen.const import DATA_DIR +from gramps.gen.const import DATA_DIR, USER_DATA from gramps.gen.datehandler import set_format from gramps.gen.user import User from gramps.gen.utils.config import config TREE_NAME = "Test_exporttest" +# the following defines where to find the test import and result files TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests")) +DB_DIR = os.path.join(USER_DATA, "grampsdb") +# the following defines where to find test error diffs and export result files +# this need to remain in place for developer debug +TEMP_DIR = os.path.join(USER_DATA, "temp") +if not os.path.isdir(TEMP_DIR): + os.makedirs(TEMP_DIR) def mock_localtime(*args): @@ -58,23 +65,20 @@ def do_it(srcfile, tstfile, dfilter=None): """ tst_file = os.path.join(TEST_DIR, srcfile) expect_file = os.path.join(TEST_DIR, tstfile) - with tempfile.TemporaryDirectory() as tmpdirname: - result_file = os.path.join(tmpdirname, tstfile) - err = call( - "-C", TREE_NAME, "-q", "--import", tst_file, "--export", result_file - )[1] - if "Cleaning up." not in err: - return "Export failed, no 'Cleaning up.'" - msg = compare(expect_file, result_file, dfilter) - if not msg: - # we will leave the result_file in place if there was an error. - try: - os.remove(result_file) - except OSError: - pass - return - else: - return msg + result_file = os.path.join(TEMP_DIR, tstfile) + err = call("-C", TREE_NAME, "-q", "--import", tst_file, "--export", result_file)[1] + if "Cleaning up." not in err: + return "Export failed, no 'Cleaning up.'" + msg = compare(expect_file, result_file, dfilter) + if not msg: + # we will leave the result_file in place if there was an error. + try: + os.remove(result_file) + except OSError: + pass + return + else: + return msg def compare(expect_file, result_file, dfilter=None): @@ -222,7 +226,11 @@ 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""" diff --git a/gramps/plugins/test/imports_test.py b/gramps/plugins/test/imports_test.py index 9078677af5b..b61b860a766 100644 --- a/gramps/plugins/test/imports_test.py +++ b/gramps/plugins/test/imports_test.py @@ -304,6 +304,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") diff --git a/gramps/plugins/test/reports_test.py b/gramps/plugins/test/reports_test.py index 075bccc86bf..95a513752d6 100644 --- a/gramps/plugins/test/reports_test.py +++ b/gramps/plugins/test/reports_test.py @@ -452,8 +452,9 @@ def test_output_file(out, dummy, report_name, **options): ) txt_list = [ - "6 media objects were referenced, but not found", - "References to 6 missing media objects were kept", + "No errors were found" + # "6 media objects were referenced, but not found", + # "References to 6 missing media objects were kept", ] reports.addcli( TestDynamic, diff --git a/gramps/plugins/test/tools_test.py b/gramps/plugins/test/tools_test.py index 947b45b2771..cb28b9eeaaa 100644 --- a/gramps/plugins/test/tools_test.py +++ b/gramps/plugins/test/tools_test.py @@ -34,6 +34,9 @@ TREE_NAME = "Test_tooltest" TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests")) +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): @@ -145,17 +148,20 @@ def test_tcg_and_check_and_repair(self): self.assertTrue(check_res(out, err, expect, do_out=True)) out, err = call("-O", TREE_NAME, "-y", "-a", "tool", "-p", "name=check") expect = [ + "7 broken child/family links were fixed", + "4 broken spouse/family links were fixed", + "1 corrupted family relationship fixed", "1 place alternate name fixed", - "9 media objects were referenced, but not found", - "References to 9 missing media objects were kept", + "3 media objects were referenced, but not found", + "References to 3 missing media objects were kept", "3 events were referenced, but not found", "1 invalid birth event name was fixed", "1 invalid death event name was fixed", "2 places were referenced, but not found", - "16 citations were referenced, but not found", - "19 sources were referenced, but not found", + "18 citations were referenced, but not found", + "21 sources were referenced, but not found", "9 Duplicated Gramps IDs fixed", - "9 empty objects removed:", + "9 empty objects removed", "1 person objects", "1 family objects", "1 event objects", diff --git a/gramps/plugins/textreport/ancestorreport.py b/gramps/plugins/textreport/ancestorreport.py index 5152be101f9..b61b5162f67 100644 --- a/gramps/plugins/textreport/ancestorreport.py +++ b/gramps/plugins/textreport/ancestorreport.py @@ -126,9 +126,13 @@ 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.database, + use_fulldate=True, + nlocale=self._locale, + place_format=place_format, ) def apply_filter(self, person_handle, index, generation=1): @@ -323,6 +327,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 308e407d078..0aaa345bf59 100644 --- a/gramps/plugins/textreport/placereport.py +++ b/gramps/plugins/textreport/placereport.py @@ -90,7 +90,7 @@ 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) @@ -188,25 +188,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 + # translators: needed for French, ignore otherwise place_details.append( - self._("%(str1)s: %(str2)s") - % {"str1": self._(level[1].xml_str()), "str2": level[0]} + _("%(str1)s: %(str2)s") + % {"str1": level[1].str(self.locale), "str2": level[0]} ) place_names = "" - all_names = place.get_all_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._("All Names: %s", "places") % 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._("All Names:", "places") + 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._("All Types:", "places") + place_types) self.doc.start_paragraph("PLC-PlaceTitle") place_title = _pd.display(self._db, place, None, self.place_format) self.doc.write_text( @@ -266,7 +273,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: @@ -348,7 +355,7 @@ def __write_referenced_persons(self, handle): 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() @@ -383,6 +390,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: @@ -574,7 +583,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/tool/check.py b/gramps/plugins/tool/check.py index a3aedddb2ca..203d6572b41 100644 --- a/gramps/plugins/tool/check.py +++ b/gramps/plugins/tool/check.py @@ -70,6 +70,7 @@ Note, Person, Place, + PlaceName, Repository, Source, StyledText, @@ -216,7 +217,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() @@ -488,36 +489,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.db.get_number_of_places() + _("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(): - if not name.value or name == place.name or name in fixed_alt_names: + for name in place.get_names(): + if not name.value or 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", self.place_errors - ) + logging.info(" %d bad place names found and fixed", self.place_errors) def check_for_broken_family_links(self): # Check persons referenced by the family objects @@ -1695,7 +1696,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: @@ -1709,7 +1710,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: @@ -1723,7 +1724,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: @@ -1737,7 +1738,7 @@ 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: @@ -1751,7 +1752,7 @@ def check_citation_references(self): if item[0] == "Citation": if not item[1]: new_handle = create_id() - repository.replace_citation_references(None, new_handle) + repository.replace_citation_references(item[1], new_handle) self.db.commit_repository(repository, self.trans) self.invalid_citation_references.add(new_handle) elif item[1] not in known_handles: @@ -1765,7 +1766,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: @@ -1779,7 +1780,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: diff --git a/gramps/plugins/tool/testcasegenerator.py b/gramps/plugins/tool/testcasegenerator.py index a44ee909c40..4c9811997ef 100644 --- a/gramps/plugins/tool/testcasegenerator.py +++ b/gramps/plugins/tool/testcasegenerator.py @@ -253,6 +253,30 @@ class TestcaseGenerator(tool.BatchTool): ] ) + 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: @@ -480,6 +504,33 @@ def run_tool(self, cli=False): 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 @@ -660,7 +711,8 @@ def test_fix_alt_place_names(self): 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): @@ -1420,7 +1472,18 @@ 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) + # 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() mref.set_reference_handle(med.handle) @@ -1432,6 +1495,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 : EventRef + place.add_event_ref(eref) self.db.add_place(place, self.trans) ref = Repository() @@ -1654,6 +1719,7 @@ def generate_family(self, person1_h): if person2_h and _randint(0, 2) > 0: self.parents_todo.append(person2_h) + self.transaction_count += 1 fam = Family() self.add_defaults(fam) if person1_h: @@ -1757,6 +1823,7 @@ def generate_parents(self, child_h): if _randint(0, 2) > 1: self.parents_todo.append(person2_h) + self.transaction_count += 1 fam = Family() self.add_defaults(fam) fam.set_father_handle(person1_h) @@ -2032,7 +2099,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)) @@ -2192,7 +2258,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: @@ -2201,6 +2267,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()) diff --git a/gramps/plugins/view/geoplaces.py b/gramps/plugins/view/geoplaces.py index 6fd9e94ea31..d24e4bcaa0f 100644 --- a/gramps/plugins/view/geoplaces.py +++ b/gramps/plugins/view/geoplaces.py @@ -48,7 +48,7 @@ from gi.repository import Gtk from gramps.gen.const import GRAMPS_LOCALE as glocale 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 @@ -179,29 +179,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): @@ -307,15 +284,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 Exception: - colour = self.plc_color[PlaceType.CUSTOM + 1] + colour = place.get_type().get_color() self._append_to_places_list( descr, None, @@ -360,30 +329,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 : @@ -403,7 +348,6 @@ def _createmap(self, place_x): "%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: @@ -584,77 +528,3 @@ def get_default_gramplets(self): Define the default gramplets for the sidebar and bottombar. """ 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 Exception: - 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 b8975fc5b99..a72c75904a0 100644 --- a/gramps/plugins/view/placetreeview.py +++ b/gramps/plugins/view/placetreeview.py @@ -117,6 +117,7 @@ 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() diff --git a/gramps/plugins/webreport/basepage.py b/gramps/plugins/webreport/basepage.py index 63d5e733286..dc3ffe636ac 100644 --- a/gramps/plugins/webreport/basepage.py +++ b/gramps/plugins/webreport/basepage.py @@ -69,7 +69,7 @@ Person, UrlType, Name, - PlaceType, + PlaceGroupType, EventRoleType, Source, Attribute, @@ -104,7 +104,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, coord_formats -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, @@ -233,6 +233,8 @@ def __init__(self, report, the_lang, the_title, gid=None): self.dir = "rtl" if self.rlocale.rtl_locale else "ltr" 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 @@ -1125,7 +1127,7 @@ def append_to_place_lat_long(self, place, event, place_lat_long): 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, fmt=0) + placetitle = _pd.display(self.r_db, place, fmt=self.place_format) latitude = place.get_latitude() longitude = place.get_longitude() if latitude and longitude: @@ -1261,7 +1263,9 @@ def get_event_data(self, evt, evt_ref, uplink, gid=None): place_hyper = None if place: - place_name = _pd.display(self.r_db, place, evt.get_date_object(), fmt=0) + 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) evt_desc = evt.get_description() @@ -1333,7 +1337,9 @@ def dump_ordinance(self, ldsobj, ldssealedtype, toggle=True): if place_handle: place = self.r_db.get_place_from_handle(place_handle) if place: - place_title = _pd.display(self.r_db, place, fmt=0) + place_title = _pd.display( + self.r_db, place, fmt=self.place_format + ) place_hyper = self.place_link( place_handle, place_title, @@ -2505,7 +2511,7 @@ def media_ref_rect_regions(self, handle, linkurl=True): _linkurl = self.report.build_url_fname_html(_obj.handle, "evt", True) elif classname == "Place": _obj = self.r_db.get_place_from_handle(newhandle) - _name = _pd.display(self.r_db, _obj, fmt=0) + _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, "plc", True) @@ -3417,24 +3423,54 @@ 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(): @@ -3451,7 +3487,7 @@ 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.") @@ -3463,6 +3499,7 @@ def dump_place(self, place, table): class_="ColumnAttribute", inline=True, ), + Html("th", date_msg, colspan=1, class_="ColumnAttribute", inline=True), Html( "th", self._("Language"), @@ -3470,14 +3507,49 @@ def dump_place(self, place, table): class_="ColumnAttribute", inline=True, ), - Html("th", date_msg, colspan=1, class_="ColumnAttribute", inline=True), + 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), ) tbody += trow diff --git a/gramps/plugins/webreport/common.py b/gramps/plugins/webreport/common.py index e10f585a525..86c6f75483b 100644 --- a/gramps/plugins/webreport/common.py +++ b/gramps/plugins/webreport/common.py @@ -1006,12 +1006,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 @@ -1025,7 +1026,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 diff --git a/gramps/plugins/webreport/heatmap.py b/gramps/plugins/webreport/heatmap.py index 0f8b8e9774d..293f1306820 100644 --- a/gramps/plugins/webreport/heatmap.py +++ b/gramps/plugins/webreport/heatmap.py @@ -904,7 +904,8 @@ def check_place(self, place, event): else: for place_ref in place.get_placeref_list(): place_new = self.r_db.get_place_from_handle(place_ref.ref) - self.check_place(place_new, event) + if place_new: + self.check_place(place_new, event) def create_tracelife(self, places, handles): number_markers = len(places) diff --git a/gramps/plugins/webreport/narrativeweb.py b/gramps/plugins/webreport/narrativeweb.py index d6d30303103..d451b8ebe98 100644 --- a/gramps/plugins/webreport/narrativeweb.py +++ b/gramps/plugins/webreport/narrativeweb.py @@ -219,6 +219,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"] @@ -1048,7 +1051,7 @@ 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, fmt=0) + place_name = _pd.display_event(self._db, event, fmt=self.place_format) if event and place_name: cplace_name = place_name.split()[-1] if len(place_name.split()) > 1: @@ -1384,10 +1387,14 @@ 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")) @@ -2298,6 +2305,7 @@ def __add_report_display(self, menu): addopt("multitrans", self.__multitrans) self.__multitrans.connect("value-changed", self.__activate_translations) + 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 65ebc35f1d2..4a87cf54137 100644 --- a/gramps/plugins/webreport/place.py +++ b/gramps/plugins/webreport/place.py @@ -49,12 +49,12 @@ # Gramps module # ------------------------------------------------ from gramps.gen.const import GRAMPS_LOCALE as glocale -from gramps.gen.lib import PlaceType, Place, PlaceName, Media +from gramps.gen.lib import PlaceGroupType as P_G, Place, PlaceName, Media from gramps.gen.plug.report import Bibliography from gramps.gen.mime import is_image_type from gramps.plugins.lib.libhtml import Html from gramps.gen.utils.place import conv_lat_lon, coord_formats -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 # ------------------------------------------------ @@ -144,19 +144,26 @@ def display_pages(self, the_lang, the_title): if place_ref not in self.report.obj_dict[Place]: place = self.r_db.get_place_from_handle(place_ref) if place: - place_name = place.get_title() + p_name = _pd.display(self.r_db, place, fmt=0) p_fname = self.report.build_url_fname( place_ref, "plc", False, init=True ) p_fname += self.ext - plc_dict = (p_fname, place_name, place.gramps_id, None) + plc_dict = (p_fname, p_name, place.gramps_id, None) self.report.obj_dict[Place][place_ref] = plc_dict - p_name = _pd.display(self.r_db, place, fmt=0) - cplace_name = p_name.split()[-1] - if len(place_name.split()) > 1: - splace_name = place_name.split()[-2] - else: - splace_name = cplace_name + loc_list = get_location_list(self.r_db, place) + splace_name = cplace_name = "" + 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 cplace_name: + # should find smaller of country group + cplace_name = name + continue + elif group == P_G.REGION: + # should find largest (state) + splace_name = name + continue plc_dict = ( place_ref, p_name, @@ -364,16 +371,7 @@ def part_placelistpage( # begin table body for place_handle, pname in places_handle_list: val = self.report.obj_dict[PlaceName][pname] - nbelem = len(val) - if val and nbelem > 3: - if isinstance(place_handle, tuple): - place = self.r_db.get_place_from_handle(place_handle[0]) - else: - place = self.r_db.get_place_from_handle(place_handle) - main_location = get_main_location(self.r_db, place) - sname = main_location.get(PlaceType.STATE, "") - cname = main_location.get(PlaceType.COUNTRY, "") - elif nbelem == 3: + if val: cname = val[3] sname = val[2] else: diff --git a/gramps/test/test_util.py b/gramps/test/test_util.py index b79fd60ade5..77e8cc44a90 100644 --- a/gramps/test/test_util.py +++ b/gramps/test/test_util.py @@ -251,10 +251,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 diff --git a/setup.py b/setup.py index 518a0fa66be..a5862734948 100755 --- a/setup.py +++ b/setup.py @@ -242,7 +242,9 @@ def run(self): raise RuntimeError( "No build directory. Run `python setup.py build` before trying to run tests." ) - os.environ["GRAMPS_RESOURCES"] = "." + # Note: GRAMPS_RESOURCES needs to be a non-relative path, else it interferes with + # the tests finding media (thinks they are relative to the db directory) + os.environ["GRAMPS_RESOURCES"] = os.path.abspath(".") all_tests = unittest.TestLoader().discover(".", pattern="*_test.py") result = unittest.TextTestRunner(verbosity=self.verbose).run(all_tests) if not result.wasSuccessful():