1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 200?-2013  Benny Malengier
6# Copyright (C) 2009       Douglas S. Blank
7# Copyright (C) 2010-2011  Nick Hall
8# Copyright (C) 2011       Michiel D. Nauta
9# Copyright (C) 2011       Tim G L Lyons
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24#
25
26#-------------------------------------------------------------------------
27#
28# Standard Python Modules
29#
30#-------------------------------------------------------------------------
31import os
32import sys
33import time
34from xml.parsers.expat import ExpatError, ParserCreate
35from xml.sax.saxutils import escape
36from gramps.gen.const import URL_WIKISTRING
37from gramps.gen.const import GRAMPS_LOCALE as glocale
38_ = glocale.translation.gettext
39import re
40import logging
41from collections import abc
42LOG = logging.getLogger(".ImportXML")
43
44#-------------------------------------------------------------------------
45#
46# Gramps Modules
47#
48#-------------------------------------------------------------------------
49from gramps.gen.mime import get_type
50from gramps.gen.lib import (Address, Attribute, AttributeType, ChildRef,
51                            ChildRefType, Citation, Date, DateError, Event,
52                            EventRef, EventRoleType, EventType, Family, LdsOrd,
53                            Location, Media, MediaRef, Name,
54                            NameOriginType, NameType, Note, NoteType, Person,
55                            PersonRef, Place, PlaceName, PlaceRef, PlaceType,
56                            RepoRef, Repository, Researcher, Source,
57                            SrcAttribute, SrcAttributeType, StyledText,
58                            StyledTextTag, StyledTextTagType, Surname, Tag, Url)
59from gramps.gen.db import DbTxn
60#from gramps.gen.db.write import CLASS_TO_KEY_MAP
61from gramps.gen.errors import GrampsImportError
62from gramps.gen.utils.id import create_id
63from gramps.gen.utils.db import family_name
64from gramps.gen.utils.unknown import make_unknown, create_explanation_note
65from gramps.gen.utils.file import create_checksum, media_path, expand_media_path
66from gramps.gen.datehandler import parser, set_date
67from gramps.gen.display.name import displayer as name_displayer
68from gramps.gen.db.dbconst import (PERSON_KEY, FAMILY_KEY, SOURCE_KEY,
69                                   EVENT_KEY, MEDIA_KEY, PLACE_KEY,
70                                   REPOSITORY_KEY, NOTE_KEY, TAG_KEY,
71                                   CITATION_KEY, CLASS_TO_KEY_MAP)
72from gramps.gen.updatecallback import UpdateCallback
73from gramps.version import VERSION
74from gramps.gen.config import config
75#import gramps.plugins.lib.libgrampsxml
76from gramps.plugins.lib import libgrampsxml
77from gramps.gen.plug.utils import version_str_to_tup
78from gramps.plugins.lib.libplaceimport import PlaceImport
79
80#-------------------------------------------------------------------------
81#
82# Try to detect the presence of gzip
83#
84#-------------------------------------------------------------------------
85try:
86    import gzip
87    GZIP_OK = True
88except:
89    GZIP_OK = False
90
91PERSON_RE = re.compile(r"\s*\<person\s(.*)$")
92
93CHILD_REL_MAP = {
94    "Birth"     : ChildRefType(ChildRefType.BIRTH),
95    "Adopted"   : ChildRefType(ChildRefType.ADOPTED),
96    "Stepchild" : ChildRefType(ChildRefType.STEPCHILD),
97    "Sponsored" : ChildRefType(ChildRefType.SPONSORED),
98    "Foster"    : ChildRefType(ChildRefType.FOSTER),
99    "Unknown"   : ChildRefType(ChildRefType.UNKNOWN),
100    }
101
102# feature requests 2356, 1658: avoid genitive form
103EVENT_FAMILY_STR = _("%(event_name)s of %(family)s")
104# feature requests 2356, 1658: avoid genitive form
105EVENT_PERSON_STR = _("%(event_name)s of %(person)s")
106
107HANDLE = 0
108INSTANTIATED = 1
109
110#-------------------------------------------------------------------------
111#
112# Importing data into the currently open database.
113# Must takes care of renaming media files according to their new IDs.
114#
115#-------------------------------------------------------------------------
116def importData(database, filename, user):
117    filename = os.path.normpath(filename)
118    basefile = os.path.dirname(filename)
119    database.smap = {}
120    database.pmap = {}
121    database.fmap = {}
122    line_cnt = 1
123    person_cnt = 0
124
125    with ImportOpenFileContextManager(filename, user) as xml_file:
126        if xml_file is None:
127            return
128
129        if filename == '-':
130            change = time.time()
131        else:
132            change = os.path.getmtime(filename)
133        if database.get_feature("skip-import-additions"): # don't add source or tags
134            parser = GrampsParser(database, user, change, None)
135        else:
136            parser = GrampsParser(database, user, change,
137                                  (config.get('preferences.tag-on-import-format') if
138                                   config.get('preferences.tag-on-import') else None))
139
140        if filename != '-':
141            linecounter = LineParser(filename)
142            line_cnt = linecounter.get_count()
143            person_cnt = linecounter.get_person_count()
144
145        read_only = database.readonly
146        database.readonly = False
147
148        try:
149            info = parser.parse(xml_file, line_cnt, person_cnt)
150        except GrampsImportError as err: # version error
151            user.notify_error(*err.messages())
152            return
153        except IOError as msg:
154            user.notify_error(_("Error reading %s") % filename, str(msg))
155            import traceback
156            traceback.print_exc()
157            return
158        except ExpatError as msg:
159            user.notify_error(_("Error reading %s") % filename,
160                        str(msg) + "\n" +
161                        _("The file is probably either corrupt or not a "
162                          "valid Gramps database."))
163            return
164
165    database.readonly = read_only
166    return info
167
168
169#-------------------------------------------------------------------------
170#
171# Remove extraneous spaces
172#
173#-------------------------------------------------------------------------
174
175def rs(text):
176    return ' '.join(text.split())
177
178def fix_spaces(text_list):
179    return '\n'.join(map(rs, text_list))
180
181#-------------------------------------------------------------------------
182#
183#
184#
185#-------------------------------------------------------------------------
186
187class ImportInfo:
188    """
189    Class object that can hold information about the import
190    """
191    keyorder = [PERSON_KEY, FAMILY_KEY, SOURCE_KEY, EVENT_KEY, MEDIA_KEY,
192                PLACE_KEY, REPOSITORY_KEY, NOTE_KEY, TAG_KEY, CITATION_KEY]
193    key2data = {
194            PERSON_KEY : 0,
195            FAMILY_KEY : 1,
196            SOURCE_KEY: 2,
197            EVENT_KEY: 3,
198            MEDIA_KEY: 4,
199            PLACE_KEY: 5,
200            REPOSITORY_KEY: 6,
201            NOTE_KEY: 7,
202            TAG_KEY: 8,
203            CITATION_KEY: 9
204            }
205
206    def __init__(self):
207        """
208        Init of the import class.
209
210        This creates the datastructures to hold info
211        """
212        self.data_mergecandidate = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
213        self.data_newobject = [0] * 10
214        self.data_unknownobject = [0] * 10
215        self.data_families = ''
216        self.expl_note = ''
217        self.data_relpath = False
218
219    def add(self, category, key, obj, sec_obj=None):
220        """
221        Add info of a certain category. Key is one of the predefined keys,
222        while obj is an object of which information will be extracted
223        """
224        if category == 'merge-candidate':
225            self.data_mergecandidate[self.key2data[key]][obj.handle] = \
226                    self._extract_mergeinfo(key, obj, sec_obj)
227        elif category == 'new-object':
228            self.data_newobject[self.key2data[key]] += 1
229        elif category == 'unknown-object':
230            self.data_unknownobject[self.key2data[key]] += 1
231        elif category == 'relative-path':
232            self.data_relpath = True
233        elif category == 'unlinked-family':
234            # This is a bit ugly because it isn't using key in the same way as
235            # the rest of the categories, but it is only the calling routine
236            # that really knows what the error message should be.
237            self.data_families += key + "\n"
238
239    def _extract_mergeinfo(self, key, obj, sec_obj):
240        """
241        Extract info from obj about 'merge-candidate', Key is one of the
242        predefined keys.
243        """
244        if key == PERSON_KEY:
245            return _("  %(id)s - %(text)s with %(id2)s\n") % {
246                        'id': obj.gramps_id,
247                        'text' : name_displayer.display(obj),
248                        'id2': sec_obj.gramps_id
249                        }
250        elif key == FAMILY_KEY :
251            return _("  Family %(id)s with %(id2)s\n") % {
252                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
253        elif key == SOURCE_KEY:
254            return _("  Source %(id)s with %(id2)s\n") % {
255                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
256        elif key == EVENT_KEY:
257            return _("  Event %(id)s with %(id2)s\n") % {
258                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
259        elif key == MEDIA_KEY:
260            return _("  Media Object %(id)s with %(id2)s\n") % {
261                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
262        elif key == PLACE_KEY:
263            return _("  Place %(id)s with %(id2)s\n") % {
264                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
265        elif key == REPOSITORY_KEY:
266            return _("  Repository %(id)s with %(id2)s\n") % {
267                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
268        elif key == NOTE_KEY:
269            return _("  Note %(id)s with %(id2)s\n") % {
270                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
271        elif key == TAG_KEY:
272            pass # Tags can't be merged
273        elif key == CITATION_KEY:
274            return _("  Citation %(id)s with %(id2)s\n") % {
275                        'id': obj.gramps_id, 'id2': sec_obj.gramps_id}
276
277    def info_text(self):
278        """
279        Construct an info message from the data in the class.
280        """
281        key2string = {
282            PERSON_KEY      : _('  People: %d\n'),
283            FAMILY_KEY      : _('  Families: %d\n'),
284            SOURCE_KEY      : _('  Sources: %d\n'),
285            EVENT_KEY       : _('  Events: %d\n'),
286            MEDIA_KEY       : _('  Media Objects: %d\n'),
287            PLACE_KEY       : _('  Places: %d\n'),
288            REPOSITORY_KEY  : _('  Repositories: %d\n'),
289            NOTE_KEY        : _('  Notes: %d\n'),
290            TAG_KEY         : _('  Tags: %d\n'),
291            CITATION_KEY    : _('  Citations: %d\n'),
292            }
293        txt = _("Number of new objects imported:\n")
294        for key in self.keyorder:
295            if any(self.data_unknownobject):
296                strng = key2string[key][0:-1] + ' (%d)\n'
297                txt += strng % (self.data_newobject[self.key2data[key]],
298                                self.data_unknownobject[self.key2data[key]])
299            else:
300                txt += key2string[key] % self.data_newobject[self.key2data[key]]
301        if any(self.data_unknownobject):
302            txt += _("\n The imported file was not self-contained.\n"
303                     "To correct for that, %(new)d objects were created and\n"
304                     "their typifying attribute was set to 'Unknown'.\n"
305                     "The breakdown per category is depicted by the\n"
306                     "number in parentheses. Where possible these\n"
307                     "'Unkown' objects are referenced by note %(unknown)s.\n"
308                     ) % {'new': sum(self.data_unknownobject), 'unknown': self.expl_note}
309        if self.data_relpath:
310            txt += _("\nMedia objects with relative paths have been\n"
311                     "imported. These paths are considered relative to\n"
312                     "the media directory you can set in the preferences,\n"
313                     "or, if not set, relative to the user's directory.\n"
314                    )
315        merge = False
316        for key in self.keyorder:
317            if self.data_mergecandidate[self.key2data[key]]:
318                merge = True
319                break
320        if merge:
321            txt += _("\n\nObjects that are candidates to be merged:\n")
322            for key in self.keyorder:
323                datakey = self.key2data[key]
324                for handle in list(self.data_mergecandidate[datakey].keys()):
325                    txt += self.data_mergecandidate[datakey][handle]
326
327        if self.data_families:
328            txt += "\n\n"
329            txt += self.data_families
330
331        return txt
332
333class LineParser:
334    def __init__(self, filename):
335
336        self.count = 0
337        self.person_count = 0
338
339        if GZIP_OK:
340            use_gzip = 1
341            try:
342                with gzip.open(filename, "r") as f:
343                    f.read(1)
344            except IOError as msg:
345                use_gzip = 0
346            except ValueError as msg:
347                use_gzip = 1
348        else:
349            use_gzip = 0
350
351        try:
352            if use_gzip:
353                import io
354                # Bug 6255. TextIOWrapper is required for python3 to
355                #           present file contents as text, otherwise they
356                #           are read as binary. However due to a missing
357                #           method (read1) in early python3 versions this
358                #           try block will fail.
359                #           Gramps will still import XML files using python
360                #           versions < 3.3.0 but the file progress meter
361                #           will not work properly, going immediately to
362                #           100%.
363                #           It should work correctly from version 3.3.
364                ofile = io.TextIOWrapper(gzip.open(filename, "rb"),
365                                         encoding='utf8', errors='replace')
366            else:
367                ofile = open(filename, "r", encoding='utf8', errors='replace')
368
369            for line in ofile:
370                self.count += 1
371                if PERSON_RE.match(line):
372                    self.person_count += 1
373        except:
374            self.count = 0
375            self.person_count = 0
376        finally:
377            # Ensure the file handle is always closed
378            ofile.close()
379
380    def get_count(self):
381        return self.count
382
383    def get_person_count(self):
384        return self.person_count
385
386#-------------------------------------------------------------------------
387#
388# ImportOpenFileContextManager
389#
390#-------------------------------------------------------------------------
391class ImportOpenFileContextManager:
392    """
393    Context manager to open a file or stdin for reading.
394    """
395    def __init__(self, filename, user):
396        self.filename = filename
397        self.filehandle = None
398        self.user = user
399
400    def __enter__(self):
401        if self.filename == '-':
402            try:
403                self.filehandle = sys.stdin.buffer
404            except:
405                self.filehandle = sys.stdin
406        else:
407            self.filehandle = self.open_file(self.filename)
408        return self.filehandle
409
410    def __exit__(self, exc_type, exc_value, traceback):
411        if self.filename != '-':
412            if self.filehandle:
413                self.filehandle.close()
414        return False
415
416    def open_file(self, filename):
417        """
418        Open the xml file.
419        Return a valid file handle if the file opened sucessfully.
420        Return None if the file was not able to be opened.
421        """
422        if GZIP_OK:
423            use_gzip = True
424            try:
425                with gzip.open(filename, "r") as ofile:
426                    ofile.read(1)
427            except IOError as msg:
428                use_gzip = False
429            except ValueError as msg:
430                use_gzip = True
431        else:
432            use_gzip = False
433
434        try:
435            if use_gzip:
436                xml_file = gzip.open(filename, "rb")
437            else:
438                xml_file = open(filename, "rb")
439        except IOError as msg:
440            self.user.notify_error(_("%s could not be opened") % filename, str(msg))
441            xml_file = None
442        except:
443            self.user.notify_error(_("%s could not be opened") % filename)
444            xml_file = None
445
446        return xml_file
447
448#-------------------------------------------------------------------------
449#
450# Gramps database parsing class.  Derived from SAX XML parser
451#
452#-------------------------------------------------------------------------
453class GrampsParser(UpdateCallback):
454
455    def __init__(self, database, user, change, default_tag_format=None):
456        UpdateCallback.__init__(self, user.callback)
457        self.user = user
458        self.__gramps_version = 'unknown'
459        self.__xml_version = (1, 0, 0)
460        self.stext_list = []
461        self.scomments_list = []
462        self.note_list = []
463        self.tlist = []
464        self.conf = 2
465        self.gid2id = {}
466        self.gid2fid = {}
467        self.gid2eid = {}
468        self.gid2pid = {}
469        self.gid2oid = {}
470        self.gid2sid = {}
471        self.gid2rid = {}
472        self.gid2nid = {}
473        self.childref_map = {}
474        self.change = change
475        self.dp = parser
476        self.info = ImportInfo()
477        self.all_abs = True
478        self.db = database
479        # Data with handles already present in the db will overwrite existing
480        # data, so all imported data gets a new handle. This behavior is not
481        # needed and even unwanted if data is imported in an empty family tree
482        # because NarWeb urls are based on handles. Also for debugging purposes
483        # it can be advantageous to preserve the orginal handle.
484        self.replace_import_handle = (self.db.get_number_of_people() > 0 and
485                                      not LOG.isEnabledFor(logging.DEBUG))
486
487        # Similarly, if the data is imported into an empty family tree, we also
488        # import the Researcher; if the tree was not empty, the existing
489        # Researcher is retained
490        self.import_researcher = self.db.get_total() == 0
491        self.ord = None
492        self.objref = None
493        self.object = None
494        self.repo = None
495        self.reporef = None
496        self.pref = None
497        self.use_p = 0
498        self.in_note = 0
499        self.in_stext = 0
500        self.in_scomments = 0
501        self.note = None
502        self.note_text = None
503        self.note_tags = []
504        self.in_witness = False
505        self.photo = None
506        self.person = None
507        self.family = None
508        self.address = None
509        self.citation = None
510        self.in_old_sourceref = False
511        self.source = None
512        self.attribute = None
513        self.srcattribute = None
514        self.placeobj = None
515        self.placeref = None
516        self.place_name = None
517        self.locations = 0
518        self.place_names = 0
519        self.place_map = {}
520        self.place_import = PlaceImport(self.db)
521
522        self.resname = ""
523        self.resaddr = ""
524        self.reslocality = ""
525        self.rescity = ""
526        self.resstate = ""
527        self.rescon = ""
528        self.respos = ""
529        self.resphone = ""
530        self.resemail = ""
531
532        self.mediapath = ""
533
534        self.pmap = {}
535        self.fmap = {}
536        self.smap = {}
537        self.lmap = {}
538        self.media_file_map = {}
539
540        # List of new name formats and a dict for remapping them
541        self.name_formats = []
542        self.name_formats_map = {}
543        self.taken_name_format_numbers = [num[0]
544                                          for num in self.db.name_formats]
545
546        self.event = None
547        self.eventref = None
548        self.childref = None
549        self.personref = None
550        self.name = None
551        self.surname = None
552        self.surnamepat = None
553        self.home = None
554        self.owner = Researcher()
555        self.func_list = [None]*50
556        self.func_index = 0
557        self.func = None
558        self.witness_comment = ""
559        self.idswap = {}
560        self.fidswap = {}
561        self.eidswap = {}
562        self.cidswap = {}
563        self.sidswap = {}
564        self.pidswap = {}
565        self.oidswap = {}
566        self.ridswap = {}
567        self.nidswap = {}
568        self.eidswap = {}
569        self.import_handles = {}
570
571        if default_tag_format:
572            name = time.strftime(default_tag_format)
573            tag = self.db.get_tag_from_name(name)
574            if tag:
575                self.default_tag = tag
576            else:
577                self.default_tag = Tag()
578                self.default_tag.set_name(name)
579        else:
580            self.default_tag = None
581
582        self.func_map = {
583            #name part
584            "name": (self.start_name, self.stop_name),
585            "first": (None, self.stop_first),
586            "call": (None, self.stop_call),
587            "aka": (self.start_name, self.stop_aka),     #deprecated < 1.3.0
588            "last": (self.start_last, self.stop_last),   #deprecated in 1.4.0
589            "nick": (None, self.stop_nick),
590            "title": (None, self.stop_title),
591            "suffix": (None, self.stop_suffix),
592            "patronymic": (self.start_patronymic, self.stop_patronymic),  #deprecated in 1.4.0
593            "familynick": (None, self.stop_familynick),  #new in 1.4.0
594            "group": (None, self.stop_group),            #new in 1.4.0, replaces attribute
595            #new in 1.4.0
596            "surname": (self.start_surname, self.stop_surname),
597            #
598            "namemaps": (None, None),
599            "name-formats": (None, None),
600            #other
601            "address": (self.start_address, self.stop_address),
602            "addresses": (None, None),
603            "alt_name": (None, self.stop_alt_name),
604            "childlist": (None, None),
605            "attribute": (self.start_attribute, self.stop_attribute),
606            "attr_type": (None, self.stop_attr_type),
607            "attr_value": (None, self.stop_attr_value),
608            "srcattribute": (self.start_srcattribute, self.stop_srcattribute),
609            "bookmark": (self.start_bmark, None),
610            "bookmarks": (None, None),
611            "format": (self.start_format, None),
612            "child": (self.start_child, None),
613            "childof": (self.start_childof, None),
614            "childref": (self.start_childref, self.stop_childref),
615            "personref": (self.start_personref, self.stop_personref),
616            "citation": (self.start_citation, self.stop_citation),
617            "citationref": (self.start_citationref, None),
618            "citations": (None, None),
619            "city": (None, self.stop_city),
620            "county": (None, self.stop_county),
621            "country": (None, self.stop_country),
622            "comment": (None, self.stop_comment),
623            "confidence": (None, self.stop_confidence),
624            "created": (self.start_created, None),
625            "ref": (None, self.stop_ref),
626            "database": (self.start_database, self.stop_database),
627            "phone": (None, self.stop_phone),
628            "date": (None, self.stop_date),
629            "cause": (None, self.stop_cause),
630            "code": (None, self.stop_code),
631            "description": (None, self.stop_description),
632            "event": (self.start_event, self.stop_event),
633            "type": (None, self.stop_type),
634            "witness": (self.start_witness, self.stop_witness),
635            "eventref": (self.start_eventref, self.stop_eventref),
636            "data_item": (self.start_data_item, None),     #deprecated in 1.6.0
637            "families": (None, self.stop_families),
638            "family": (self.start_family, self.stop_family),
639            "rel": (self.start_rel, None),
640            "region": (self.start_region, None),
641            "father": (self.start_father, None),
642            "gender": (None, self.stop_gender),
643            "header": (None, self.stop_header),
644            "map": (self.start_namemap, None),
645            "mediapath": (None, self.stop_mediapath),
646            "mother": (self.start_mother, None),
647            "note": (self.start_note, self.stop_note),
648            "noteref": (self.start_noteref, None),
649            "p": (None, self.stop_ptag),
650            "parentin": (self.start_parentin, None),
651            "people": (self.start_people, self.stop_people),
652            "person": (self.start_person, self.stop_person),
653            "img": (self.start_photo, self.stop_photo),
654            "objref": (self.start_objref, self.stop_objref),
655            "object": (self.start_media, self.stop_media),
656            "file": (self.start_file, None),
657            "page": (None, self.stop_page),
658            "place": (self.start_place, self.stop_place),
659            "dateval": (self.start_dateval, None),
660            "daterange": (self.start_daterange, None),
661            "datespan": (self.start_datespan, None),
662            "datestr": (self.start_datestr, None),
663            "places": (None, self.stop_places),
664            "placeobj": (self.start_placeobj, self.stop_placeobj),
665            "placeref": (self.start_placeref, self.stop_placeref),
666            "ptitle": (None, self.stop_ptitle),
667            "pname": (self.start_place_name, self.stop_place_name),
668            "locality": (None, self.stop_locality),
669            "location": (self.start_location, None),
670            "lds_ord": (self.start_lds_ord, self.stop_lds_ord),
671            "temple": (self.start_temple, None),
672            "status": (self.start_status, None),
673            "sealed_to": (self.start_sealed_to, None),
674            "coord": (self.start_coord, None),
675            "pos": (self.start_pos, None),
676            "postal": (None, self.stop_postal),
677            "range": (self.start_range, None),
678            "researcher": (None, self.stop_research),
679            "resname": (None, self.stop_resname),
680            "resaddr": (None, self.stop_resaddr),
681            "reslocality": (None, self.stop_reslocality),
682            "rescity": (None, self.stop_rescity),
683            "resstate": (None, self.stop_resstate),
684            "rescountry": (None, self.stop_rescountry),
685            "respostal": (None, self.stop_respostal),
686            "resphone": (None, self.stop_resphone),
687            "resemail": (None, self.stop_resemail),
688            "sauthor": (None, self.stop_sauthor),
689            "sabbrev": (None, self.stop_sabbrev),
690            "scomments": (None, self.stop_scomments),
691            "source": (self.start_source, self.stop_source),
692            "sourceref": (self.start_sourceref, self.stop_sourceref),
693            "sources": (None, None),
694            "spage": (None, self.stop_spage),
695            "spubinfo": (None, self.stop_spubinfo),
696            "state": (None, self.stop_state),
697            "stext": (None, self.stop_stext),
698            "stitle": (None, self.stop_stitle),
699            "street": (None, self.stop_street),
700            "style": (self.start_style, None),
701            "tag": (self.start_tag, self.stop_tag),
702            "tagref": (self.start_tagref, None),
703            "tags": (None, None),
704            "text": (None, self.stop_text),
705            "url": (self.start_url, None),
706            "repository": (self.start_repo, self.stop_repo),
707            "reporef": (self.start_reporef, self.stop_reporef),
708            "rname": (None, self.stop_rname),
709        }
710        self.grampsuri = re.compile(r"^gramps://(?P<object_class>[A-Z][a-z]+)/"
711                                    r"handle/(?P<handle>\w+)$")
712
713    def inaugurate(self, handle, target, prim_obj):
714        """
715        Assign a handle (identity) to a primary object (and create it if it
716        doesn't exist yet) and add it to the database.
717
718        This method can be called with an object instance or with a
719        class object. Be aware that in the first case the side effect of this
720        function is to fill the object instance with the data read from the db.
721        In the second case, an empty object with the correct handle will be
722        created.
723
724        :param handle: The handle of the primary object, typically as read
725                       directly from the XML attributes.
726        :type handle: str
727        :param target: Indicates the primary object type this handle relates to.
728        :type target: str, identical to target attr of bookmarks.
729        :param prim_obj: template of the primary object that is to be created.
730        :type prim_obj: Either an empty instance of a primary object or the
731                         class object of a primary object.
732        :returns: The handle of the primary object.
733        :rtype: str
734        """
735        handle = str(handle.replace('_', ''))
736        orig_handle = handle
737        if (orig_handle in self.import_handles and
738                target in self.import_handles[orig_handle]):
739            handle = self.import_handles[handle][target][HANDLE]
740            if not isinstance(prim_obj, abc.Callable):
741                # This method is called by a start_<primary_object> method.
742                get_raw_obj_data = {"person": self.db.get_raw_person_data,
743                                    "family": self.db.get_raw_family_data,
744                                    "event": self.db.get_raw_event_data,
745                                    "place": self.db.get_raw_place_data,
746                                    "source": self.db.get_raw_source_data,
747                                    "citation": self.db.get_raw_citation_data,
748                                    "repository": self.db.get_raw_repository_data,
749                                    "media": self.db.get_raw_media_data,
750                                    "note": self.db.get_raw_note_data,
751                                    "tag": self.db.get_raw_tag_data}[target]
752                raw = get_raw_obj_data(handle)
753                prim_obj.unserialize(raw)
754                self.import_handles[orig_handle][target][INSTANTIATED] = True
755            return handle
756        elif handle in self.import_handles:
757            LOG.warning("The file you import contains duplicate handles "
758                        "which is illegal and being fixed now.")
759            handle = create_id()
760            while handle in self.import_handles:
761                handle = create_id()
762            self.import_handles[orig_handle][target] = [handle, False]
763        else:
764            orig_handle = handle
765            if self.replace_import_handle:
766                handle = create_id()
767                while handle in self.import_handles:
768                    handle = create_id()
769            else:
770                has_handle_func = {"person": self.db.has_person_handle,
771                                   "family": self.db.has_family_handle,
772                                   "event": self.db.has_event_handle,
773                                   "place": self.db.has_place_handle,
774                                   "source": self.db.has_source_handle,
775                                   "citation": self.db.get_raw_citation_data,
776                                   "repository": self.db.has_repository_handle,
777                                   "media": self.db.has_media_handle,
778                                   "note": self.db.has_note_handle,
779                                   "tag": self.db.has_tag_handle}[target]
780                while has_handle_func(handle):
781                    handle = create_id()
782            self.import_handles[orig_handle] = {target: [handle, False]}
783        # method is called by a reference
784        if isinstance(prim_obj, abc.Callable):
785            prim_obj = prim_obj()
786        else:
787            self.import_handles[orig_handle][target][INSTANTIATED] = True
788        prim_obj.set_handle(handle)
789        if target == "tag":
790            self.db.add_tag(prim_obj, self.trans)
791        else:
792            add_func = {"person": self.db.add_person,
793                        "family": self.db.add_family,
794                        "event": self.db.add_event,
795                        "place": self.db.add_place,
796                        "source": self.db.add_source,
797                        "citation": self.db.add_citation,
798                        "repository": self.db.add_repository,
799                        "media": self.db.add_media,
800                        "note": self.db.add_note}[target]
801            add_func(prim_obj, self.trans, set_gid=False)
802        return handle
803
804    def inaugurate_id(self, id_, key, prim_obj):
805        """
806        Equivalent of inaugurate but for old style XML.
807        """
808        if id_ is None:
809            raise GrampsImportError(_("The Gramps Xml you are trying to "
810                "import is malformed."), _("Attributes that link the data "
811                "together are missing."))
812        id2handle_map = [self.gid2id, self.gid2fid, self.gid2sid,
813                         self.gid2eid, self.gid2oid, self.gid2pid,
814                         self.gid2rid, 'reference', self.gid2nid][key]
815        has_handle_func = [self.db.has_person_handle,
816                           self.db.has_family_handle,
817                           self.db.has_source_handle,
818                           self.db.has_event_handle,
819                           self.db.has_media_handle,
820                           self.db.has_place_handle,
821                           self.db.has_repository_handle,
822                           'reference',
823                           self.db.has_note_handle][key]
824        add_func = [self.db.add_person,
825                    self.db.add_family,
826                    self.db.add_source,
827                    self.db.add_event,
828                    self.db.add_media,
829                    self.db.add_place,
830                    self.db.add_repository,
831                    'reference',
832                    self.db.add_note][key]
833        get_raw_obj_data = [self.db.get_raw_person_data,
834                            self.db.get_raw_family_data,
835                            self.db.get_raw_source_data,
836                            self.db.get_raw_event_data,
837                            self.db.get_raw_media_data,
838                            self.db.get_raw_place_data,
839                            self.db.get_raw_repository_data,
840                            'reference', self.db.get_raw_note_data][key]
841        id2id_map = [self.idswap, self.fidswap, self.sidswap, self.eidswap,
842                     self.oidswap, self.pidswap, self.ridswap, 'reference',
843                     self.nidswap][key]
844        id2user_format = [self.db.id2user_format, self.db.fid2user_format,
845                          self.db.sid2user_format, self.db.eid2user_format,
846                          self.db.oid2user_format, self.db.pid2user_format,
847                          self.db.rid2user_format, 'reference',
848                          self.db.nid2user_format][key]
849        find_next_gramps_id = [self.db.find_next_person_gramps_id,
850                               self.db.find_next_family_gramps_id,
851                               self.db.find_next_source_gramps_id,
852                               self.db.find_next_event_gramps_id,
853                               self.db.find_next_media_gramps_id,
854                               self.db.find_next_place_gramps_id,
855                               self.db.find_next_repository_gramps_id,
856                               'reference',
857                               self.db.find_next_note_gramps_id][key]
858        has_gramps_id = [self.db.has_person_gramps_id,
859                         self.db.has_family_gramps_id,
860                         self.db.has_source_gramps_id,
861                         self.db.has_event_gramps_id,
862                         self.db.has_media_gramps_id,
863                         self.db.has_place_gramps_id,
864                         self.db.has_repository_gramps_id,
865                         'reference',
866                         self.db.has_note_gramps_id][key]
867
868        gramps_id = self.legalize_id(id_, key, id2id_map, id2user_format,
869                                     find_next_gramps_id, has_gramps_id)
870        handle = id2handle_map.get(gramps_id)
871        if handle:
872            raw = get_raw_obj_data(handle)
873            prim_obj.unserialize(raw)
874        else:
875            handle = create_id()
876            while has_handle_func(handle):
877                handle = create_id()
878            if isinstance(prim_obj, abc.Callable):
879                prim_obj = prim_obj()
880            prim_obj.set_handle(handle)
881            prim_obj.set_gramps_id(gramps_id)
882            add_func(prim_obj, self.trans)
883            id2handle_map[gramps_id] = handle
884        return handle
885
886    def legalize_id(self, id_, key, gramps_ids, id2user_format,
887                    find_next_gramps_id, has_gramps_id):
888        """
889        Given an import id, adjust it so that it fits with the existing data.
890
891        :param id_: The id as it is in the Xml import file, might be None.
892        :type id_: str
893        :param key: Indicates kind of primary object this id is for.
894        :type key: int
895        :param gramps_ids: Dictionary with id's that have already been imported.
896        :type import_ids: dict
897        :param id2user_format: Function to convert a raw id into the format as
898                               specified in the prefixes.
899        :type id2user_format: func
900        :param find_next_gramps_id: function to get the next available id.
901        :type find_next_gramps_id: func
902        :returns: The id.
903        :rtype: str
904        """
905        gramps_id = id2user_format(id_)
906        if gramps_id is None or not gramps_ids.get(id_):
907            if gramps_id is None or has_gramps_id(gramps_id):
908                gramps_ids[id_] = find_next_gramps_id()
909            else:
910                gramps_ids[id_] = gramps_id
911        return gramps_ids[id_]
912
913    def parse(self, ifile, linecount=1, personcount=0):
914        """
915        Parse the xml file
916        :param ifile: must be a file handle that is already open, with position
917                      at the start of the file
918        """
919        if personcount < 1000:
920            no_magic = True
921        else:
922            no_magic = False
923        with DbTxn(_("Gramps XML import"), self.db, batch=True,
924                   no_magic=no_magic) as self.trans:
925            self.set_total(linecount)
926
927            self.db.disable_signals()
928
929            if self.default_tag and self.default_tag.handle is None:
930                self.db.add_tag(self.default_tag, self.trans)
931
932            self.p = ParserCreate()
933            self.p.StartElementHandler = self.startElement
934            self.p.EndElementHandler = self.endElement
935            self.p.CharacterDataHandler = self.characters
936            self.p.ParseFile(ifile)
937
938            if len(self.name_formats) > 0:
939                # add new name formats to the existing table
940                self.db.name_formats += self.name_formats
941                # Register new formats
942                name_displayer.set_name_format(self.db.name_formats)
943
944            # If the database was originally empty we update the researcher from
945            # the XML (or initialised to no researcher)
946            if self.import_researcher:
947                self.db.set_researcher(self.owner)
948            if self.home is not None:
949                person = self.db.get_person_from_handle(self.home)
950                self.db.set_default_person_handle(person.handle)
951
952            # Set media path
953            # The paths are normalized before being compared.
954            if self.mediapath:
955                if not self.db.get_mediapath():
956                    self.db.set_mediapath(self.mediapath)
957                elif not media_path(self.db) == expand_media_path(self.mediapath, self.db):
958                    self.user.notify_error(_("Could not change media path"),
959                        _("The opened file has media path %s, which conflicts with"
960                          " the media path of the Family Tree you import into. "
961                          "The original media path has been retained. Copy the "
962                          "files to a correct directory or change the media "
963                          "path in the Preferences."
964                         ) % self.mediapath )
965
966            self.fix_not_instantiated()
967            self.fix_families()
968            for key in list(self.func_map.keys()):
969                del self.func_map[key]
970            del self.func_map
971            del self.func_list
972            del self.p
973            del self.update
974        self.db.enable_signals()
975        self.db.request_rebuild()
976        return self.info
977
978    def start_database(self, attrs):
979        """
980        Get the xml version of the file.
981        """
982        if 'xmlns' in attrs:
983            xmlns = attrs.get('xmlns').split('/')
984            if len(xmlns)>= 2 and not xmlns[2] == 'gramps-project.org':
985                self.__xml_version = (0, 0, 0)
986            else:
987                try:
988                    self.__xml_version = version_str_to_tup(xmlns[4], 3)
989                except:
990                    #leave version at 1.0.0 although it could be 0.0.0 ??
991                    pass
992        else:
993            #1.0 or before xml, no dtd schema yet on
994            # http://www.gramps-project.org/xml/
995            self.__xml_version = (0, 0, 0)
996
997    def start_created(self, attrs):
998        """
999        Get the Gramps version that produced the file.
1000        """
1001        if 'sources' in attrs:
1002            self.num_srcs = int(attrs['sources'])
1003        else:
1004            self.num_srcs = 0
1005        if 'places' in attrs:
1006            self.num_places = int(attrs['places'])
1007        else:
1008            self.num_places = 0
1009        if 'version' in attrs:
1010            self.__gramps_version = attrs.get('version')
1011
1012    def stop_header(self, *dummy):
1013        """
1014        Check the version of Gramps and XML.
1015        """
1016        xmlversion_str = '.'.join(str(i) for i in self.__xml_version)
1017        if self.__gramps_version == 'unknown':
1018            msg = _("The .gramps file you are importing does not contain information about "
1019                    "the version of Gramps with, which it was produced.\n\n"
1020                    "The file will not be imported.")
1021            raise GrampsImportError(_('Import file misses Gramps version'), msg)
1022        if self.__xml_version > libgrampsxml.GRAMPS_XML_VERSION_TUPLE:
1023            msg = _("The .gramps file you are importing was made by "
1024                    "version %(newer)s of "
1025                    "Gramps, while you are running an older version %(older)s. "
1026                    "The file will not be imported. Please upgrade to the "
1027                    "latest version of Gramps and try again." ) % {
1028                    'newer' : self.__gramps_version, 'older' : VERSION }
1029            raise GrampsImportError('', msg)
1030        if self.__xml_version < (1, 0, 0):
1031            msg = _("The .gramps file you are importing was made by version "
1032                    "%(oldgramps)s of Gramps, while you are running a more "
1033                    "recent version %(newgramps)s.\n\n"
1034                    "The file will not be imported. Please use an older version"
1035                    " of Gramps that supports version %(xmlversion)s of the "
1036                    "xml.\nSee\n  %(gramps_wiki_xml_url)s\n for more info."
1037                    ) % {'oldgramps': self.__gramps_version,
1038                        'newgramps': VERSION,
1039                        'xmlversion': xmlversion_str,
1040                        'gramps_wiki_xml_url': URL_WIKISTRING + "Gramps_XML" ,
1041                        }
1042            raise GrampsImportError(_('The file will not be imported'), msg)
1043        elif self.__xml_version < (1, 1, 0):
1044            msg = _("The .gramps file you are importing was made by version "
1045                    "%(oldgramps)s of Gramps, while you are running a much "
1046                    "more recent version %(newgramps)s.\n\n"
1047                    "Ensure after import everything is imported correctly. In "
1048                    "the event of problems, please submit a bug and use an "
1049                    "older version of Gramps in the meantime to import this "
1050                    "file, which is version %(xmlversion)s of the xml.\nSee\n  "
1051                    "%(gramps_wiki_xml_url)s\nfor more info."
1052                    ) % {'oldgramps': self.__gramps_version,
1053                        'newgramps': VERSION,
1054                        'xmlversion': xmlversion_str,
1055                        'gramps_wiki_xml_url': URL_WIKISTRING + "Gramps_XML" ,
1056                        }
1057            self.user.warn(_('Old xml file'), msg)
1058
1059    def start_lds_ord(self, attrs):
1060        self.ord = LdsOrd()
1061        self.ord.set_type_from_xml(attrs['type'])
1062        self.ord.private = bool(attrs.get("priv"))
1063        if self.person:
1064            self.person.lds_ord_list.append(self.ord)
1065        elif self.family:
1066            self.family.lds_ord_list.append(self.ord)
1067
1068    def start_temple(self, attrs):
1069        self.ord.set_temple(attrs['val'])
1070
1071    def start_data_item(self, attrs):
1072        """
1073        Deprecated in 1.6.0, replaced by srcattribute
1074        """
1075        sat = SrcAttributeType(attrs['key'])
1076        sa = SrcAttribute()
1077        sa.set_type(sat)
1078        sa.set_value(attrs['value'])
1079        if self.source:
1080            self.source.add_attribute(sa)
1081        else:
1082            self.citation.add_attribute(sa)
1083
1084    def start_status(self, attrs):
1085        try:
1086            # old xml with integer statuses
1087            self.ord.set_status(int(attrs['val']))
1088        except ValueError:
1089            # string
1090            self.ord.set_status_from_xml(attrs['val'])
1091
1092    def start_sealed_to(self, attrs):
1093        """
1094        Add a family reference to the LDS ordinance currently processed.
1095        """
1096        if 'hlink' in attrs:
1097            handle = self.inaugurate(attrs['hlink'], "family", Family)
1098        else: # old style XML
1099            handle = self.inaugurate_id(attrs.get('ref'), FAMILY_KEY,
1100                                        Family)
1101        self.ord.set_family_handle(handle)
1102
1103    def start_place(self, attrs):
1104        """A reference to a place in an object: event or lds_ord
1105        """
1106        if 'hlink' in attrs:
1107            handle = self.inaugurate(attrs['hlink'], "place", Place)
1108        else: # old style XML
1109            handle = self.inaugurate_id(attrs.get('ref'), PLACE_KEY,
1110                                        Place)
1111        if self.ord:
1112            self.ord.set_place_handle(handle)
1113        elif self.event:
1114            self.event.set_place_handle(handle)
1115
1116    def start_placeobj(self, attrs):
1117        """
1118        Add a place object to db if it doesn't exist yet and assign
1119        id, privacy and changetime.
1120        """
1121        self.placeobj = Place()
1122        if 'handle' in attrs:
1123            orig_handle = attrs['handle'].replace('_', '')
1124            is_merge_candidate = (self.replace_import_handle and
1125                                  self.db.has_place_handle(orig_handle))
1126            self.inaugurate(orig_handle, "place", self.placeobj)
1127            gramps_id = self.legalize_id(attrs.get('id'), PLACE_KEY,
1128                                         self.pidswap, self.db.pid2user_format,
1129                                         self.db.find_next_place_gramps_id,
1130                                         self.db.has_place_gramps_id)
1131            self.placeobj.set_gramps_id(gramps_id)
1132            if is_merge_candidate:
1133                orig_place = self.db.get_place_from_handle(orig_handle)
1134                self.info.add('merge-candidate', PLACE_KEY, orig_place,
1135                              self.placeobj)
1136        else:
1137            self.inaugurate_id(attrs.get('id'), PLACE_KEY, self.placeobj)
1138        self.placeobj.private = bool(attrs.get("priv"))
1139        self.placeobj.change = int(attrs.get('change', self.change))
1140        if self.__xml_version == (1, 6, 0):
1141            place_name = PlaceName()
1142            place_name.set_value(attrs.get('name', ''))
1143            self.placeobj.name = place_name
1144        if 'type' in attrs:
1145            self.placeobj.place_type.set_from_xml_str(attrs.get('type'))
1146        self.info.add('new-object', PLACE_KEY, self.placeobj)
1147        self.place_names = 0
1148
1149        # Gramps LEGACY: title in the placeobj tag
1150        self.placeobj.title = attrs.get('title', '')
1151        self.locations = 0
1152        self.update(self.p.CurrentLineNumber)
1153        if self.default_tag:
1154            self.placeobj.add_tag(self.default_tag.handle)
1155        return self.placeobj
1156
1157    def start_location(self, attrs):
1158        """Bypass the function calls for this one, since it appears to
1159        take up quite a bit of time"""
1160
1161        loc = Location()
1162        loc.street = attrs.get('street', '')
1163        loc.locality = attrs.get('locality', '')
1164        loc.city = attrs.get('city', '')
1165        loc.parish = attrs.get('parish', '')
1166        loc.county = attrs.get('county', '')
1167        loc.state = attrs.get('state', '')
1168        loc.country = attrs.get('country', '')
1169        loc.postal = attrs.get('postal', '')
1170        loc.phone = attrs.get('phone', '')
1171
1172        if self.__xml_version < (1, 6, 0):
1173            if self.locations > 0:
1174                self.placeobj.add_alternate_locations(loc)
1175            else:
1176                location = (attrs.get('street', ''),
1177                            attrs.get('locality', ''),
1178                            attrs.get('parish', ''),
1179                            attrs.get('city', ''),
1180                            attrs.get('county', ''),
1181                            attrs.get('state', ''),
1182                            attrs.get('country', ''))
1183                self.place_import.store_location(location, self.placeobj.handle)
1184
1185                for level, name in enumerate(location):
1186                    if name:
1187                        break
1188                place_name = PlaceName()
1189                place_name.set_value(name)
1190                self.placeobj.set_name(place_name)
1191                type_num = 7 - level if name else PlaceType.UNKNOWN
1192                self.placeobj.set_type(PlaceType(type_num))
1193                codes = [attrs.get('postal'), attrs.get('phone')]
1194                self.placeobj.set_code(' '.join(code for code in codes if code))
1195        else:
1196            self.placeobj.add_alternate_locations(loc)
1197
1198        self.locations = self.locations + 1
1199
1200    def start_witness(self, attrs):
1201        """
1202        Add a note about a witness to the currently processed event or add
1203        an event reference connecting that event with a person assigning the
1204        role of witness.
1205        """
1206        # Parse witnesses created by older gramps
1207        self.in_witness = True
1208        self.witness_comment = ""
1209        if 'name' in attrs:
1210            note = Note()
1211            note.handle = create_id()
1212            note.set(_("Witness name: %s") % attrs['name'])
1213            note.type.set(NoteType.EVENT)
1214            note.private = self.event.private
1215            self.db.add_note(note, self.trans)
1216            #set correct change time
1217            self.db.commit_note(note, self.trans, self.change)
1218            self.info.add('new-object', NOTE_KEY, note)
1219            self.event.add_note(note.handle)
1220            return
1221
1222        person = Person()
1223        if 'hlink' in attrs:
1224            self.inaugurate(attrs['hlink'], "person", person)
1225        elif 'ref' in attrs:
1226            self.inaugurate_id(attrs['ref'], PERSON_KEY, person)
1227        else:
1228            person = None
1229
1230        # Add an EventRef from that person
1231        # to this event using ROLE_WITNESS role
1232        if person:
1233            event_ref = EventRef()
1234            event_ref.ref = self.event.handle
1235            event_ref.role.set(EventRoleType.WITNESS)
1236            person.event_ref_list.append(event_ref)
1237            self.db.commit_person(person, self.trans, self.change)
1238
1239    def start_coord(self, attrs):
1240        self.placeobj.lat = attrs.get('lat', '')
1241        self.placeobj.long = attrs.get('long', '')
1242
1243    def start_event(self, attrs):
1244        """
1245        Add an event object to db if it doesn't exist yet and assign
1246        id, privacy and changetime.
1247        """
1248        if self.person or self.family:
1249            # Gramps LEGACY: old events that were written inside
1250            # person or family objects.
1251            self.event = Event()
1252            self.event.handle = create_id()
1253            self.event.type = EventType()
1254            self.event.type.set_from_xml_str(attrs['type'])
1255            self.db.add_event(self.event, self.trans)
1256            #set correct change time
1257            self.db.commit_event(self.event, self.trans, self.change)
1258            self.info.add('new-object', EVENT_KEY, self.event)
1259        else:
1260            # This is new event, with ID and handle already existing
1261            self.update(self.p.CurrentLineNumber)
1262            self.event = Event()
1263            if 'handle' in attrs:
1264                orig_handle = attrs['handle'].replace('_', '')
1265                is_merge_candidate = (self.replace_import_handle and
1266                                      self.db.has_event_handle(orig_handle))
1267                self.inaugurate(orig_handle, "event", self.event)
1268                gramps_id = self.legalize_id(attrs.get('id'), EVENT_KEY,
1269                                          self.eidswap, self.db.eid2user_format,
1270                                          self.db.find_next_event_gramps_id,
1271                                          self.db.has_event_gramps_id)
1272                self.event.set_gramps_id(gramps_id)
1273                if is_merge_candidate:
1274                    orig_event = self.db.get_event_from_handle(orig_handle)
1275                    self.info.add('merge-candidate', EVENT_KEY, orig_event,
1276                                  self.event)
1277            else: #old style XML
1278                self.inaugurate_id(attrs.get('id'), EVENT_KEY, self.event)
1279            self.event.private = bool(attrs.get("priv"))
1280            self.event.change = int(attrs.get('change', self.change))
1281            self.info.add('new-object', EVENT_KEY, self.event)
1282        if self.default_tag:
1283            self.event.add_tag(self.default_tag.handle)
1284        return self.event
1285
1286    def start_eventref(self, attrs):
1287        """
1288        Add an event reference to the object currently processed.
1289        """
1290        self.eventref = EventRef()
1291        if 'hlink' in attrs:
1292            handle = self.inaugurate(attrs['hlink'], "event", Event)
1293        else: # there is no old style XML
1294            raise GrampsImportError(_("The Gramps Xml you are trying to "
1295                "import is malformed."), _("Any event reference must have a "
1296                "'hlink' attribute."))
1297        self.eventref.ref = handle
1298        self.eventref.private = bool(attrs.get('priv'))
1299        if 'role' in attrs:
1300            self.eventref.role.set_from_xml_str(attrs['role'])
1301
1302        # We count here on events being already parsed prior to parsing
1303        # people or families. This code will fail if this is not true.
1304        event = self.db.get_event_from_handle(self.eventref.ref)
1305        if not event:
1306            return
1307
1308        if self.family:
1309            event.personal = False
1310            self.family.add_event_ref(self.eventref)
1311        elif self.person:
1312            event.personal = True
1313            if (event.type == EventType.BIRTH) \
1314                   and (self.eventref.role == EventRoleType.PRIMARY) \
1315                   and (self.person.get_birth_ref() is None):
1316                self.person.set_birth_ref(self.eventref)
1317            elif (event.type == EventType.DEATH) \
1318                     and (self.eventref.role == EventRoleType.PRIMARY) \
1319                     and (self.person.get_death_ref() is None):
1320                self.person.set_death_ref(self.eventref)
1321            else:
1322                self.person.add_event_ref(self.eventref)
1323
1324    def start_placeref(self, attrs):
1325        """
1326        Add a place reference to the place currently being processed.
1327        """
1328        self.placeref = PlaceRef()
1329        handle = self.inaugurate(attrs['hlink'], "place", Place)
1330        self.placeref.ref = handle
1331        self.placeobj.add_placeref(self.placeref)
1332
1333    def start_attribute(self, attrs):
1334        self.attribute = Attribute()
1335        self.attribute.private = bool(attrs.get("priv"))
1336        self.attribute.type = AttributeType()
1337        if 'type' in attrs:
1338            self.attribute.type.set_from_xml_str(attrs["type"])
1339        self.attribute.value = attrs.get("value", '')
1340        if self.photo:
1341            self.photo.add_attribute(self.attribute)
1342        elif self.object:
1343            self.object.add_attribute(self.attribute)
1344        elif self.objref:
1345            self.objref.add_attribute(self.attribute)
1346        elif self.event:
1347            self.event.add_attribute(self.attribute)
1348        elif self.eventref:
1349            self.eventref.add_attribute(self.attribute)
1350        elif self.person:
1351            self.person.add_attribute(self.attribute)
1352        elif self.family:
1353            self.family.add_attribute(self.attribute)
1354
1355    def start_srcattribute(self, attrs):
1356        self.srcattribute = SrcAttribute()
1357        self.srcattribute.private = bool(attrs.get("priv"))
1358        self.srcattribute.type = SrcAttributeType()
1359        if 'type' in attrs:
1360            self.srcattribute.type.set_from_xml_str(attrs["type"])
1361        self.srcattribute.value = attrs.get("value", '')
1362        if self.source:
1363            self.source.add_attribute(self.srcattribute)
1364        elif self.citation:
1365            self.citation.add_attribute(self.srcattribute)
1366
1367    def start_address(self, attrs):
1368        self.address = Address()
1369        self.address.private = bool(attrs.get("priv"))
1370
1371    def start_bmark(self, attrs):
1372        """
1373        Add a bookmark to db.
1374        """
1375        target = attrs.get('target')
1376        if not target:
1377            # Old XML. Can be either handle or id reference
1378            # and this is guaranteed to be a person bookmark
1379            if 'hlink' in attrs:
1380                handle = self.inaugurate(attrs['hlink'], "person",
1381                                         Person)
1382            else:
1383                handle = self.inaugurate_id(attrs.get('ref'), PERSON_KEY,
1384                                            Person)
1385            self.db.bookmarks.append(handle)
1386            return
1387
1388        # This is new XML, so we are guaranteed to have a handle ref
1389        handle = attrs['hlink'].replace('_', '')
1390        handle = self.import_handles[handle][target][HANDLE]
1391        # Due to pre 2.2.9 bug, bookmarks might be handle of other object
1392        # Make sure those are filtered out.
1393        # Bookmarks are at end, so all handle must exist before we do bookmrks
1394        if target == 'person':
1395            if (self.db.get_person_from_handle(handle) is not None
1396                    and handle not in self.db.bookmarks.get() ):
1397                self.db.bookmarks.append(handle)
1398        elif target == 'family':
1399            if (self.db.get_family_from_handle(handle) is not None
1400                    and handle not in self.db.family_bookmarks.get() ):
1401                self.db.family_bookmarks.append(handle)
1402        elif target == 'event':
1403            if (self.db.get_event_from_handle(handle) is not None
1404                    and handle not in self.db.event_bookmarks.get() ):
1405                self.db.event_bookmarks.append(handle)
1406        elif target == 'source':
1407            if (self.db.get_source_from_handle(handle) is not None
1408                    and handle not in self.db.source_bookmarks.get() ):
1409                self.db.source_bookmarks.append(handle)
1410        elif target == 'citation':
1411            if (self.db.get_citation_from_handle(handle) is not None
1412                    and handle not in self.db.citation_bookmarks.get() ):
1413                self.db.citation_bookmarks.append(handle)
1414        elif target == 'place':
1415            if (self.db.get_place_from_handle(handle) is not None
1416                    and handle not in self.db.place_bookmarks.get() ):
1417                self.db.place_bookmarks.append(handle)
1418        elif target == 'media':
1419            if (self.db.get_media_from_handle(handle) is not None
1420                    and handle not in self.db.media_bookmarks.get() ):
1421                self.db.media_bookmarks.append(handle)
1422        elif target == 'repository':
1423            if (self.db.get_repository_from_handle(handle)
1424                    is not None and handle not in self.db.repo_bookmarks.get()):
1425                self.db.repo_bookmarks.append(handle)
1426        elif target == 'note':
1427            if (self.db.get_note_from_handle(handle) is not None
1428                    and handle not in self.db.note_bookmarks.get() ):
1429                self.db.note_bookmarks.append(handle)
1430
1431    def start_format(self, attrs):
1432        number = int(attrs['number'])
1433        name = attrs['name']
1434        fmt_str = attrs['fmt_str']
1435        active = bool(attrs.get('active', True))
1436
1437        if number in self.taken_name_format_numbers:
1438            number = self.remap_name_format(number)
1439
1440        self.name_formats.append((number, name, fmt_str, active))
1441
1442    def remap_name_format(self, old_number):
1443        if old_number in self.name_formats_map: # This should not happen
1444            return self.name_formats_map[old_number]
1445        # Find the lowest new number not taken yet:
1446        new_number = -1
1447        while new_number in self.taken_name_format_numbers:
1448            new_number -= 1
1449        # Add this to the taken list
1450        self.taken_name_format_numbers.append(new_number)
1451        # Set up the mapping entry
1452        self.name_formats_map[old_number] = new_number
1453        # Return new number
1454        return new_number
1455
1456    def start_person(self, attrs):
1457        """
1458        Add a person to db if it doesn't exist yet and assign
1459        id, privacy and changetime.
1460        """
1461        self.update(self.p.CurrentLineNumber)
1462        self.person = Person()
1463        if 'handle' in attrs:
1464            orig_handle = attrs['handle'].replace('_', '')
1465            is_merge_candidate = (self.replace_import_handle and
1466                                  self.db.has_person_handle(orig_handle))
1467            self.inaugurate(orig_handle, "person", self.person)
1468            gramps_id = self.legalize_id(attrs.get('id'), PERSON_KEY,
1469                                        self.idswap, self.db.id2user_format,
1470                                        self.db.find_next_person_gramps_id,
1471                                        self.db.has_person_gramps_id)
1472            self.person.set_gramps_id(gramps_id)
1473            if is_merge_candidate:
1474                orig_person = self.db.get_person_from_handle(orig_handle)
1475                self.info.add('merge-candidate', PERSON_KEY, orig_person,
1476                              self.person)
1477        else: # old style XML
1478            self.inaugurate_id(attrs.get('id'), PERSON_KEY, self.person)
1479        self.person.private = bool(attrs.get("priv"))
1480        self.person.change = int(attrs.get('change', self.change))
1481        self.info.add('new-object', PERSON_KEY, self.person)
1482        self.convert_marker(attrs, self.person)
1483        if self.default_tag:
1484            self.person.add_tag(self.default_tag.handle)
1485        return self.person
1486
1487    def start_people(self, attrs):
1488        """
1489        Store the home person of the database.
1490        """
1491        if 'home' in attrs:
1492            handle = self.inaugurate(attrs['home'], "person", Person)
1493            self.home = handle
1494
1495    def start_father(self, attrs):
1496        """
1497        Add a father reference to the family currently processed.
1498        """
1499        if 'hlink' in attrs:
1500            handle = self.inaugurate(attrs['hlink'], "person", Person)
1501        else: # old style XML
1502            handle = self.inaugurate_id(attrs.get('ref'), PERSON_KEY,
1503                                        Person)
1504        self.family.set_father_handle(handle)
1505
1506    def start_mother(self, attrs):
1507        """
1508        Add a mother reference to the family currently processed.
1509        """
1510        if 'hlink' in attrs:
1511            handle = self.inaugurate(attrs['hlink'], "person", Person)
1512        else: # old style XML
1513            handle = self.inaugurate_id(attrs.get('ref'), PERSON_KEY,
1514                                        Person)
1515        self.family.set_mother_handle(handle)
1516
1517    def start_child(self, attrs):
1518        """
1519        Add a child reference to the family currently processed.
1520
1521        Here we are handling the old XML, in which
1522        frel and mrel belonged to the "childof" tag
1523        """
1524        if 'hlink' in attrs:
1525            handle = self.inaugurate(attrs['hlink'], "person", Person)
1526        else: # old style XML
1527            handle = self.inaugurate_id(attrs.get('ref'), PERSON_KEY,
1528                                        Person)
1529
1530        # If that were the case then childref_map has the childref ready
1531        if (self.family.handle, handle) in self.childref_map:
1532            self.family.add_child_ref(
1533                self.childref_map[(self.family.handle, handle)])
1534
1535    def start_childref(self, attrs):
1536        """
1537        Add a child reference to the family currently processed.
1538
1539        Here we are handling the new XML, in which frel and mrel
1540        belong to the "childref" tag under family.
1541        """
1542        self.childref = ChildRef()
1543        handle = self.inaugurate(attrs['hlink'], "person", Person)
1544        self.childref.ref = handle
1545        self.childref.private = bool(attrs.get('priv'))
1546
1547        mrel = ChildRefType()
1548        if attrs.get('mrel'):
1549            mrel.set_from_xml_str(attrs['mrel'])
1550        frel = ChildRefType()
1551        if attrs.get('frel'):
1552            frel.set_from_xml_str(attrs['frel'])
1553
1554        if not mrel.is_default():
1555            self.childref.set_mother_relation(mrel)
1556        if not frel.is_default():
1557            self.childref.set_father_relation(frel)
1558        self.family.add_child_ref(self.childref)
1559
1560    def start_personref(self, attrs):
1561        """
1562        Add a person reference to the person currently processed.
1563        """
1564        self.personref = PersonRef()
1565        if 'hlink' in attrs:
1566            handle = self.inaugurate(attrs['hlink'], "person", Person)
1567        else: # there is no old style XML
1568            raise GrampsImportError(_("The Gramps Xml you are trying to "
1569                "import is malformed."), _("Any person reference must have a "
1570                "'hlink' attribute."))
1571        self.personref.ref = handle
1572        self.personref.private = bool(attrs.get('priv'))
1573        self.personref.rel = attrs['rel']
1574        self.person.add_person_ref(self.personref)
1575
1576    def start_url(self, attrs):
1577        if "href" not in attrs:
1578            return
1579        url = Url()
1580        url.path = attrs["href"]
1581        url.set_description(attrs.get("description", ''))
1582        url.private = bool(attrs.get('priv'))
1583        url.type.set_from_xml_str(attrs.get('type', ''))
1584        if self.person:
1585            self.person.add_url(url)
1586        elif self.placeobj:
1587            self.placeobj.add_url(url)
1588        elif self.repo:
1589            self.repo.add_url(url)
1590
1591    def start_family(self, attrs):
1592        """
1593        Add a family object to db if it doesn't exist yet and assign
1594        id, privacy and changetime.
1595        """
1596        self.update(self.p.CurrentLineNumber)
1597        self.family = Family()
1598        if 'handle' in attrs:
1599            orig_handle = attrs['handle'].replace('_', '')
1600            is_merge_candidate = (self.replace_import_handle and
1601                                  self.db.has_family_handle(orig_handle))
1602            self.inaugurate(orig_handle, "family", self.family)
1603            gramps_id = self.legalize_id(attrs.get('id'), FAMILY_KEY,
1604                                        self.fidswap, self.db.fid2user_format,
1605                                        self.db.find_next_family_gramps_id,
1606                                        self.db.has_family_gramps_id)
1607            self.family.set_gramps_id(gramps_id)
1608            if is_merge_candidate:
1609                orig_family = self.db.get_family_from_handle(orig_handle)
1610                self.info.add('merge-candidate', FAMILY_KEY, orig_family,
1611                              self.family)
1612        else: # old style XML
1613            self.inaugurate_id(attrs.get('id'), FAMILY_KEY, self.family)
1614        self.family.private = bool(attrs.get("priv"))
1615        self.family.change = int(attrs.get('change', self.change))
1616        self.info.add('new-object', FAMILY_KEY, self.family)
1617        # Gramps LEGACY: the type now belongs to <rel> tag
1618        # Here we need to support old format of <family type="Married">
1619        if 'type' in attrs:
1620            self.family.type.set_from_xml_str(attrs["type"])
1621        self.convert_marker(attrs, self.family)
1622        if self.default_tag:
1623            self.family.add_tag(self.default_tag.handle)
1624        return self.family
1625
1626    def start_rel(self, attrs):
1627        if 'type' in attrs:
1628            self.family.type.set_from_xml_str(attrs["type"])
1629
1630    def start_file(self, attrs):
1631        self.object.mime = attrs['mime']
1632        if 'description' in attrs:
1633            self.object.desc = attrs['description']
1634        else:
1635            self.object.desc = ""
1636        #keep value of path, no longer make absolute paths on import
1637        src = attrs["src"]
1638        if src:
1639            self.object.path = src
1640            if self.all_abs and not os.path.isabs(src):
1641                self.all_abs = False
1642                self.info.add('relative-path', None, None)
1643        if 'checksum' in attrs:
1644            self.object.checksum = attrs['checksum']
1645        else:
1646            if os.path.isabs(src):
1647                full_path = src
1648            else:
1649                full_path = os.path.join(self.mediapath, src)
1650            self.object.checksum = create_checksum(full_path)
1651
1652    def start_childof(self, attrs):
1653        """
1654        Add a family reference to the person currently processed in which that
1655        person is a child.
1656        """
1657        if 'hlink' in attrs:
1658            handle = self.inaugurate(attrs['hlink'], "family", Family)
1659        else: # old style XML
1660            handle = self.inaugurate_id(attrs.get('ref'), FAMILY_KEY,
1661                                        Family)
1662
1663        # Here we are handling the old XML, in which
1664        # frel and mrel belonged to the "childof" tag
1665        mrel = ChildRefType()
1666        frel = ChildRefType()
1667        if 'mrel' in attrs:
1668            mrel.set_from_xml_str(attrs['mrel'])
1669        if 'frel' in attrs:
1670            frel.set_from_xml_str(attrs['frel'])
1671
1672        childref = ChildRef()
1673        childref.ref = self.person.handle
1674        if not mrel.is_default():
1675            childref.set_mother_relation(mrel)
1676        if not frel.is_default():
1677            childref.set_father_relation(frel)
1678        self.childref_map[(handle, self.person.handle)] = childref
1679        self.person.add_parent_family_handle(handle)
1680
1681    def start_parentin(self, attrs):
1682        """
1683        Add a family reference to the person currently processed in which that
1684        person is a parent.
1685        """
1686        if 'hlink' in attrs:
1687            handle = self.inaugurate(attrs['hlink'], "family", Family)
1688        else: # old style XML
1689            handle = self.inaugurate_id(attrs.get('ref'), FAMILY_KEY,
1690                                        Family)
1691        self.person.add_family_handle(handle)
1692
1693    def start_name(self, attrs):
1694        if self.person:
1695            self.start_person_name(attrs)
1696        if self.placeobj: # XML 1.7.0
1697            self.start_place_name(attrs)
1698
1699    def start_place_name(self, attrs):
1700        self.place_name = PlaceName()
1701        self.place_name.set_value(attrs["value"])
1702        if "lang" in attrs:
1703             self.place_name.set_language(attrs["lang"])
1704        if self.place_names == 0:
1705            self.placeobj.set_name(self.place_name)
1706        else:
1707            self.placeobj.add_alternative_name(self.place_name)
1708        self.place_names += 1
1709
1710    def start_person_name(self, attrs):
1711        if not self.in_witness:
1712            self.name = Name()
1713            name_type = attrs.get('type', "Birth Name")
1714            # Mapping "Other Name" from gramps 2.0.x to Unknown
1715            if (self.__xml_version == (1, 0, 0)) and (name_type == 'Other Name'):
1716                self.name.set_type(NameType.UNKNOWN)
1717            else:
1718                self.name.type.set_from_xml_str(name_type)
1719            self.name.private = bool(attrs.get("priv", 0))
1720            self.alt_name = bool(attrs.get("alt", 0))
1721            try:
1722                sort_as = int(attrs["sort"])
1723                # check if these pointers need to be remapped
1724                # and set the name attributes
1725                if sort_as in self.name_formats_map:
1726                    self.name.sort_as = self.name_formats_map[sort_as]
1727                else:
1728                    self.name.sort_as = sort_as
1729            except KeyError:
1730                pass
1731            try:
1732                display_as = int(attrs["display"])
1733                # check if these pointers need to be remapped
1734                # and set the name attributes
1735                if display_as in self.name_formats_map:
1736                    self.name.display_as = self.name_formats_map[display_as]
1737                else:
1738                    self.name.display_as = display_as
1739            except KeyError:
1740                pass
1741
1742    def start_surname(self, attrs):
1743        self.surname = Surname()
1744        self.surname.set_prefix(attrs.get("prefix", ""))
1745        self.surname.set_primary(attrs.get("prim", "1") == "1")
1746        self.surname.set_connector(attrs.get("connector", ""))
1747        origin_type = attrs.get("derivation", "")
1748        self.surname.origintype.set_from_xml_str(origin_type)
1749
1750    def start_namemap(self, attrs):
1751        type = attrs.get('type')
1752        key = attrs['key']
1753        value = attrs['value']
1754        if type == 'group_as':
1755            if self.db.has_name_group_key(key):
1756                present = self.db.get_name_group_mapping(key)
1757                if not value == present:
1758                    msg = _('Your Family Tree groups name "%(key)s" together'
1759                            ' with "%(parent)s", did not change this grouping to "%(value)s".') % {
1760                            'key' : key, 'parent' : present, 'value' : value }
1761                    self.user.warn(_("Gramps ignored a name grouping"), msg)
1762            elif value != 'None':  # None test fixes file corrupted by 11011
1763                self.db.set_name_group_mapping(key, value)
1764
1765    def start_last(self, attrs):
1766        """ This is the element in version < 1.4.0 to do the surname"""
1767        self.surname = Surname()
1768        self.surname.prefix = attrs.get('prefix', '')
1769        self.name.group_as = attrs.get('group', '')
1770
1771    def start_patronymic(self, attrs):
1772        """ This is the element in version < 1.4.0 to do the patronymic"""
1773        self.surnamepat = Surname()
1774        self.surnamepat.set_origintype(NameOriginType(
1775                                       NameOriginType.PATRONYMIC))
1776
1777    def start_style(self, attrs):
1778        """
1779        Styled text tag in notes (v1.4.0 onwards).
1780        """
1781        tagtype = StyledTextTagType()
1782        tagtype.set_from_xml_str(attrs['name'].lower())
1783        try:
1784            val = attrs['value']
1785            match = self.grampsuri.match(val)
1786            if match:
1787                target = {"Person" : "person", "Family" : "family",
1788                          "Event" : "event", "Place" : "place",
1789                          "Source" : "source", "Citation" : "citation",
1790                          "Repository" : "repository", "Media" : "media",
1791                          "Note" : "note"}[str(match.group('object_class'))]
1792                if match.group('handle') in self.import_handles:
1793                    if target in self.import_handles[match.group('handle')]:
1794                        val = "gramps://%s/handle/%s" % (
1795                                match.group('object_class'),
1796                                self.import_handles[match.group('handle')]
1797                                                   [target][HANDLE])
1798            tagvalue = StyledTextTagType.STYLE_TYPE[int(tagtype)](val)
1799        except KeyError:
1800            tagvalue = None
1801        except ValueError:
1802            return
1803
1804        self.note_tags.append(StyledTextTag(tagtype, tagvalue))
1805
1806    def start_tag(self, attrs):
1807        """
1808        Tag definition.
1809        """
1810        if self.note is not None:
1811            # Styled text tag in notes (prior to v1.4.0)
1812            self.start_style(attrs)
1813            return
1814
1815        # Tag defintion
1816        self.tag = Tag()
1817        self.inaugurate(attrs['handle'], "tag", self.tag)
1818        self.tag.change = int(attrs.get('change', self.change))
1819        self.info.add('new-object', TAG_KEY, self.tag)
1820        self.tag.set_name(attrs.get('name', _('Unknown when imported')))
1821        self.tag.set_color(attrs.get('color', '#000000000000'))
1822        self.tag.set_priority(int(attrs.get('priority', 0)))
1823        return self.tag
1824
1825    def stop_tag(self, *tag):
1826        if self.note is not None:
1827            # Styled text tag in notes (prior to v1.4.0)
1828            return
1829        self.db.commit_tag(self.tag, self.trans, self.tag.get_change_time())
1830        self.tag = None
1831
1832    def start_tagref(self, attrs):
1833        """
1834        Tag reference in a primary object.
1835        """
1836        handle = self.inaugurate(attrs['hlink'], "tag", Tag)
1837
1838        if self.person:
1839            self.person.add_tag(handle)
1840
1841        if self.family:
1842            self.family.add_tag(handle)
1843
1844        if self.object:
1845            self.object.add_tag(handle)
1846
1847        if self.note:
1848            self.note.add_tag(handle)
1849
1850        if self.event:
1851            self.event.add_tag(handle)
1852
1853        if self.placeobj:
1854            self.placeobj.add_tag(handle)
1855
1856        if self.repo:
1857            self.repo.add_tag(handle)
1858
1859        if self.source:
1860            self.source.add_tag(handle)
1861
1862        if self.citation:
1863            self.citation.add_tag(handle)
1864
1865    def start_range(self, attrs):
1866        self.note_tags[-1].ranges.append((int(attrs['start']),
1867                                          int(attrs['end'])))
1868
1869    def start_note(self, attrs):
1870        """
1871        Add a note to db if it doesn't exist yet and assign
1872        id, privacy, changetime, format and type.
1873        """
1874        self.in_note = 0
1875        if 'handle' in attrs:
1876            # This is new note, with ID and handle already existing
1877            self.update(self.p.CurrentLineNumber)
1878            self.note = Note()
1879            if 'handle' in attrs:
1880                orig_handle = attrs['handle'].replace('_', '')
1881                is_merge_candidate = (self.replace_import_handle and
1882                                      self.db.has_note_handle(orig_handle))
1883                self.inaugurate(orig_handle, "note", self.note)
1884                gramps_id = self.legalize_id(attrs.get('id'), NOTE_KEY,
1885                                          self.nidswap, self.db.nid2user_format,
1886                                          self.db.find_next_note_gramps_id,
1887                                          self.db.has_note_gramps_id)
1888                self.note.set_gramps_id(gramps_id)
1889                if is_merge_candidate:
1890                    orig_note = self.db.get_note_from_handle(orig_handle)
1891                    self.info.add('merge-candicate', NOTE_KEY, orig_note,
1892                                  self.note)
1893            else:
1894                self.inaugurate_id(attrs.get('id'), NOTE_KEY, self.note)
1895            self.note.private = bool(attrs.get("priv"))
1896            self.note.change = int(attrs.get('change', self.change))
1897            self.info.add('new-object', NOTE_KEY, self.note)
1898            self.note.format = int(attrs.get('format', Note.FLOWED))
1899            self.note.type.set_from_xml_str(attrs.get('type',
1900                                                      NoteType.UNKNOWN))
1901            self.convert_marker(attrs, self.note)
1902
1903            # Since StyledText was introduced (XML v1.3.0) the clear text
1904            # part of the note is moved between <text></text> tags.
1905            # To catch the different versions here we reset the note_text
1906            # variable. It will be checked in stop_note() then.
1907            self.note_text = None
1908            self.note_tags = []
1909        else:
1910            # Gramps LEGACY: old notes that were written inside other objects
1911            # We need to create a top-level note, it's type depends on
1912            #   the caller object, and inherits privacy from caller object
1913            # On stop_note the reference to this note will be added
1914            self.note = Note()
1915            self.note.handle = create_id()
1916            self.note.format = int(attrs.get('format', Note.FLOWED))
1917            # The order in this long if-then statement should reflect the
1918            # DTD: most deeply nested elements come first.
1919            if self.citation:
1920                self.note.type.set(NoteType.CITATION)
1921                self.note.private = self.citation.private
1922            elif self.address:
1923                self.note.type.set(NoteType.ADDRESS)
1924                self.note.private = self.address.private
1925            elif self.ord:
1926                self.note.type.set(NoteType.LDS)
1927                self.note.private = self.ord.private
1928            elif self.attribute:
1929                self.note.type.set(NoteType.ATTRIBUTE)
1930                self.note.private = self.attribute.private
1931            elif self.object:
1932                self.note.type.set(NoteType.MEDIA)
1933                self.note.private = self.object.private
1934            elif self.objref:
1935                self.note.type.set(NoteType.MEDIAREF)
1936                self.note.private = self.objref.private
1937            elif self.photo:
1938                self.note.type.set(NoteType.MEDIA)
1939                self.note.private = self.photo.private
1940            elif self.name:
1941                self.note.type.set(NoteType.PERSONNAME)
1942                self.note.private = self.name.private
1943            elif self.eventref:
1944                self.note.type.set(NoteType.EVENTREF)
1945                self.note.private = self.eventref.private
1946            elif self.reporef:
1947                self.note.type.set(NoteType.REPOREF)
1948                self.note.private = self.reporef.private
1949            elif self.source:
1950                self.note.type.set(NoteType.SOURCE)
1951                self.note.private = self.source.private
1952            elif self.event:
1953                self.note.type.set(NoteType.EVENT)
1954                self.note.private = self.event.private
1955            elif self.personref:
1956                self.note.type.set(NoteType.ASSOCIATION)
1957                self.note.private = self.personref.private
1958            elif self.person:
1959                self.note.type.set(NoteType.PERSON)
1960                self.note.private = self.person.private
1961            elif self.childref:
1962                self.note.type.set(NoteType.CHILDREF)
1963                self.note.private = self.childref.private
1964            elif self.family:
1965                self.note.type.set(NoteType.FAMILY)
1966                self.note.private = self.family.private
1967            elif self.placeobj:
1968                self.note.type.set(NoteType.PLACE)
1969                self.note.private = self.placeobj.private
1970            elif self.repo:
1971                self.note.type.set(NoteType.REPO)
1972                self.note.private = self.repo.private
1973
1974            self.db.add_note(self.note, self.trans)
1975            #set correct change time
1976            self.db.commit_note(self.note, self.trans, self.change)
1977            self.info.add('new-object', NOTE_KEY, self.note)
1978        if self.default_tag:
1979            self.note.add_tag(self.default_tag.handle)
1980        return self.note
1981
1982    def start_noteref(self, attrs):
1983        """
1984        Add a note reference to the object currently processed.
1985        """
1986        if 'hlink' in attrs:
1987            handle = self.inaugurate(attrs['hlink'], "note", Note)
1988        else:
1989            raise GrampsImportError(_("The Gramps Xml you are trying to "
1990                "import is malformed."), _("Any note reference must have a "
1991                "'hlink' attribute."))
1992
1993        # The order in this long if-then statement should reflect the
1994        # DTD: most deeply nested elements come first.
1995        if self.citation:
1996            self.citation.add_note(handle)
1997        elif self.address:
1998            self.address.add_note(handle)
1999        elif self.ord:
2000            self.ord.add_note(handle)
2001        elif self.attribute:
2002            self.attribute.add_note(handle)
2003        elif self.object:
2004            self.object.add_note(handle)
2005        elif self.objref:
2006            self.objref.add_note(handle)
2007        elif self.photo:
2008            self.photo.add_note(handle)
2009        elif self.name:
2010            self.name.add_note(handle)
2011        elif self.eventref:
2012            self.eventref.add_note(handle)
2013        elif self.reporef:
2014            self.reporef.add_note(handle)
2015        elif self.source:
2016            self.source.add_note(handle)
2017        elif self.event:
2018            self.event.add_note(handle)
2019        elif self.personref:
2020            self.personref.add_note(handle)
2021        elif self.person:
2022            self.person.add_note(handle)
2023        elif self.childref:
2024            self.childref.add_note(handle)
2025        elif self.family:
2026            self.family.add_note(handle)
2027        elif self.placeobj:
2028            self.placeobj.add_note(handle)
2029        elif self.repo:
2030            self.repo.add_note(handle)
2031
2032    def __add_citation(self, citation_handle):
2033        """
2034        Add a citation to the object currently processed.
2035        """
2036        if self.photo:
2037            self.photo.add_citation(citation_handle)
2038        elif self.ord:
2039            self.ord.add_citation(citation_handle)
2040        elif self.attribute:
2041            self.attribute.add_citation(citation_handle)
2042        elif self.object:
2043            self.object.add_citation(citation_handle)
2044        elif self.objref:
2045            self.objref.add_citation(citation_handle)
2046        elif self.event:
2047            self.event.add_citation(citation_handle)
2048        elif self.address:
2049            self.address.add_citation(citation_handle)
2050        elif self.name:
2051            self.name.add_citation(citation_handle)
2052        elif self.placeobj:
2053            self.placeobj.add_citation(citation_handle)
2054        elif self.childref:
2055            self.childref.add_citation(citation_handle)
2056        elif self.family:
2057            self.family.add_citation(citation_handle)
2058        elif self.personref:
2059            self.personref.add_citation(citation_handle)
2060        elif self.person:
2061            self.person.add_citation(citation_handle)
2062
2063    def start_citationref(self, attrs):
2064        """
2065        Add a citation reference to the object currently processed.
2066        """
2067        handle = self.inaugurate(attrs['hlink'], "citation", Citation)
2068
2069        self.__add_citation(handle)
2070
2071    def start_citation(self, attrs):
2072        """
2073        Add a citation object to db if it doesn't exist yet and assign
2074        id, privacy and changetime.
2075        """
2076        self.update(self.p.CurrentLineNumber)
2077        self.citation = Citation()
2078        orig_handle = attrs['handle'].replace('_', '')
2079        is_merge_candidate = (self.replace_import_handle and
2080                              self.db.has_citation_handle(orig_handle))
2081        self.inaugurate(orig_handle, "citation", self.citation)
2082        gramps_id = self.legalize_id(attrs.get('id'), CITATION_KEY,
2083                                     self.cidswap, self.db.cid2user_format,
2084                                     self.db.find_next_citation_gramps_id,
2085                                     self.db.has_citation_gramps_id)
2086        self.citation.set_gramps_id(gramps_id)
2087        if is_merge_candidate:
2088            orig_citation = self.db.get_citation_from_handle(orig_handle)
2089            self.info.add('merge-candidate', CITATION_KEY, orig_citation,
2090                          self.citation)
2091        self.citation.private = bool(attrs.get("priv"))
2092        self.citation.change = int(attrs.get('change', self.change))
2093        self.citation.confidence = (
2094                self.conf if self.__xml_version >= (1, 5, 1)
2095                else 0 ) # See bug# 7125
2096        self.info.add('new-object', CITATION_KEY, self.citation)
2097        if self.default_tag:
2098            self.citation.add_tag(self.default_tag.handle)
2099        return self.citation
2100
2101    def start_sourceref(self, attrs):
2102        """
2103        Add a source reference to the object currently processed.
2104        """
2105        if 'hlink' in attrs:
2106            handle = self.inaugurate(attrs['hlink'], "source", Source)
2107        else:
2108            handle = self.inaugurate_id(attrs.get('ref'), SOURCE_KEY,
2109                                        Source)
2110
2111        if self.citation:
2112            self.citation.set_reference_handle(handle)
2113        else:
2114            # Gramps LEGACY: Prior to v1.5.0 there were no citation objects.
2115            # We need to copy the contents of the old SourceRef into a new
2116            # Citation object.
2117            self.in_old_sourceref = True
2118
2119            self.citation = Citation()
2120            self.citation.set_reference_handle(handle)
2121            self.citation.confidence = int(attrs.get("conf", self.conf))
2122            self.citation.private = bool(attrs.get("priv"))
2123
2124            citation_handle = self.db.add_citation(self.citation, self.trans)
2125            self.__add_citation(citation_handle)
2126
2127    def start_source(self, attrs):
2128        """
2129        Add a source object to db if it doesn't exist yet and assign
2130        id, privacy and changetime.
2131        """
2132        self.update(self.p.CurrentLineNumber)
2133        self.source = Source()
2134        if 'handle' in attrs:
2135            orig_handle = attrs['handle'].replace('_', '')
2136            is_merge_candidate = (self.replace_import_handle and
2137                                  self.db.has_source_handle(orig_handle))
2138            self.inaugurate(orig_handle, "source", self.source)
2139            gramps_id = self.legalize_id(attrs.get('id'), SOURCE_KEY,
2140                                         self.sidswap, self.db.sid2user_format,
2141                                         self.db.find_next_source_gramps_id,
2142                                         self.db.has_source_gramps_id)
2143            self.source.set_gramps_id(gramps_id)
2144            if is_merge_candidate:
2145                orig_source = self.db.get_source_from_handle(orig_handle)
2146                self.info.add('merge-candidate', SOURCE_KEY, orig_source,
2147                              self.source)
2148        else: # old style XML
2149            self.inaugurate_id(attrs.get('id'), SOURCE_KEY, self.source)
2150        self.source.private = bool(attrs.get("priv"))
2151        self.source.change = int(attrs.get('change', self.change))
2152        self.info.add('new-object', SOURCE_KEY, self.source)
2153        if self.default_tag:
2154            self.source.add_tag(self.default_tag.handle)
2155        return self.source
2156
2157    def start_reporef(self, attrs):
2158        """
2159        Add a repository reference to the source currently processed.
2160        """
2161        self.reporef = RepoRef()
2162        if 'hlink' in attrs:
2163            handle = self.inaugurate(attrs['hlink'], "repository",
2164                                     Repository)
2165        else: # old style XML
2166            handle = self.inaugurate_id(attrs.get('ref'), REPOSITORY_KEY,
2167                                        Repository)
2168        self.reporef.ref = handle
2169        self.reporef.call_number = attrs.get('callno', '')
2170        if 'medium' in attrs:
2171            self.reporef.media_type.set_from_xml_str(attrs['medium'])
2172        self.reporef.private = bool(attrs.get("priv"))
2173        # we count here on self.source being available
2174        # reporefs can only be found within source
2175        self.source.add_repo_reference(self.reporef)
2176
2177    def start_objref(self, attrs):
2178        """
2179        Add a media object reference to the object currently processed.
2180        """
2181        self.objref = MediaRef()
2182        if 'hlink' in attrs:
2183            handle = self.inaugurate(attrs['hlink'], "media",
2184                                     Media)
2185        else: # old style XML
2186            handle = self.inaugurate_id(attrs.get('ref'), MEDIA_KEY,
2187                                        Media)
2188        self.objref.ref = handle
2189        self.objref.private = bool(attrs.get('priv'))
2190        if self.event:
2191            self.event.add_media_reference(self.objref)
2192        elif self.family:
2193            self.family.add_media_reference(self.objref)
2194        elif self.source:
2195            self.source.add_media_reference(self.objref)
2196        elif self.person:
2197            self.person.add_media_reference(self.objref)
2198        elif self.placeobj:
2199            self.placeobj.add_media_reference(self.objref)
2200        elif self.citation:
2201            self.citation.add_media_reference(self.objref)
2202
2203    def start_region(self, attrs):
2204        rect = (int(attrs.get('corner1_x')),
2205                int(attrs.get('corner1_y')),
2206                int(attrs.get('corner2_x')),
2207                int(attrs.get('corner2_y')) )
2208        self.objref.set_rectangle(rect)
2209
2210    def start_media(self, attrs):
2211        """
2212        Add a media object to db if it doesn't exist yet and assign
2213        id, privacy and changetime.
2214        """
2215        self.object = Media()
2216        if 'handle' in attrs:
2217            orig_handle = attrs['handle'].replace('_', '')
2218            is_merge_candidate = (self.replace_import_handle and
2219                                  self.db.has_media_handle(orig_handle))
2220            self.inaugurate(orig_handle, "media", self.object)
2221            gramps_id = self.legalize_id(attrs.get('id'), MEDIA_KEY,
2222                                         self.oidswap, self.db.oid2user_format,
2223                                         self.db.find_next_media_gramps_id,
2224                                         self.db.has_media_gramps_id)
2225            self.object.set_gramps_id(gramps_id)
2226            if is_merge_candidate:
2227                orig_media = self.db.get_media_from_handle(orig_handle)
2228                self.info.add('merge-candidate', MEDIA_KEY, orig_media,
2229                              self.object)
2230        else:
2231            self.inaugurate_id(attrs.get('id'), MEDIA_KEY, self.object)
2232        self.object.private = bool(attrs.get("priv"))
2233        self.object.change = int(attrs.get('change', self.change))
2234        self.info.add('new-object', MEDIA_KEY, self.object)
2235
2236        # Gramps LEGACY: src, mime, and description attributes
2237        # now belong to the <file> tag. Here we are supporting
2238        # the old format of <object src="blah"...>
2239        self.object.mime = attrs.get('mime', '')
2240        self.object.desc = attrs.get('description', '')
2241        src = attrs.get("src", '')
2242        if src:
2243            self.object.path = src
2244        if self.default_tag:
2245            self.object.add_tag(self.default_tag.handle)
2246        return self.object
2247
2248    def start_repo(self, attrs):
2249        """
2250        Add a repository to db if it doesn't exist yet and assign
2251        id, privacy and changetime.
2252        """
2253        self.repo = Repository()
2254        if 'handle' in attrs:
2255            orig_handle = attrs['handle'].replace('_', '')
2256            is_merge_candidate = (self.replace_import_handle and
2257                                  self.db.has_repository_handle(orig_handle))
2258            self.inaugurate(orig_handle, "repository", self.repo)
2259            gramps_id = self.legalize_id(attrs.get('id'), REPOSITORY_KEY,
2260                                         self.ridswap, self.db.rid2user_format,
2261                                         self.db.find_next_repository_gramps_id,
2262                                         self.db.has_repository_gramps_id)
2263            self.repo.set_gramps_id(gramps_id)
2264            if is_merge_candidate:
2265                orig_repo = self.db.get_repository_from_handle(orig_handle)
2266                self.info.add('merge-candidate', REPOSITORY_KEY, orig_repo,
2267                              self.repo)
2268        else: # old style XML
2269            self.inaugurate_id(attrs.get('id'), REPOSITORY_KEY, self.repo)
2270        self.repo.private = bool(attrs.get("priv"))
2271        self.repo.change = int(attrs.get('change', self.change))
2272        self.info.add('new-object', REPOSITORY_KEY, self.repo)
2273        if self.default_tag:
2274            self.repo.add_tag(self.default_tag.handle)
2275        return self.repo
2276
2277    def stop_people(self, *tag):
2278        pass
2279
2280    def stop_database(self, *tag):
2281        self.update(self.p.CurrentLineNumber)
2282
2283    def stop_media(self, *tag):
2284        self.db.commit_media(self.object, self.trans,
2285                                    self.object.get_change_time())
2286        self.object = None
2287
2288    def stop_objref(self, *tag):
2289        self.objref = None
2290
2291    def stop_repo(self, *tag):
2292        self.db.commit_repository(self.repo, self.trans,
2293                                  self.repo.get_change_time())
2294        self.repo = None
2295
2296    def stop_reporef(self, *tag):
2297        self.reporef = None
2298
2299    def start_photo(self, attrs):
2300        self.photo = Media()
2301        self.pref = MediaRef()
2302        self.pref.set_reference_handle(self.photo.get_handle())
2303
2304        for key in list(attrs.keys()):
2305            if key == "descrip" or key == "description":
2306                self.photo.set_description(attrs[key])
2307            elif key == "priv":
2308                self.pref.set_privacy(int(attrs[key]))
2309                self.photo.set_privacy(int(attrs[key]))
2310            elif key == "src":
2311                src = attrs["src"]
2312                self.photo.set_path(src)
2313            else:
2314                attr = Attribute()
2315                attr.set_type(key)
2316                attr.set_value(attrs[key])
2317                self.photo.add_attribute(attr)
2318        self.photo.set_mime_type(get_type(self.photo.get_path()))
2319        self.db.add_media(self.photo, self.trans)
2320        #set correct change time
2321        self.db.commit_media(self.photo, self.trans, self.change)
2322        self.info.add('new-object', MEDIA_KEY, self.photo)
2323        if self.family:
2324            self.family.add_media_reference(self.pref)
2325        elif self.source:
2326            self.source.add_media_reference(self.pref)
2327        elif self.person:
2328            self.person.add_media_reference(self.pref)
2329        elif self.placeobj:
2330            self.placeobj.add_media_reference(self.pref)
2331
2332    def start_daterange(self, attrs):
2333        self.start_compound_date(attrs, Date.MOD_RANGE)
2334
2335    def start_datespan(self, attrs):
2336        self.start_compound_date(attrs, Date.MOD_SPAN)
2337
2338    def start_compound_date(self, attrs, mode):
2339        if self.citation:
2340            date_value = self.citation.get_date_object()
2341        elif self.ord:
2342            date_value = self.ord.get_date_object()
2343        elif self.object:
2344            date_value = self.object.get_date_object()
2345        elif self.address:
2346            date_value = self.address.get_date_object()
2347        elif self.name:
2348            date_value = self.name.get_date_object()
2349        elif self.event:
2350            date_value = self.event.get_date_object()
2351        elif self.placeref:
2352            date_value = self.placeref.get_date_object()
2353        elif self.place_name:
2354            date_value = self.place_name.get_date_object()
2355
2356        start = attrs['start'].split('-')
2357        stop = attrs['stop'].split('-')
2358
2359        try:
2360            year = int(start[0])
2361        except ValueError:
2362            year = 0
2363
2364        try:
2365            month = int(start[1])
2366        except:
2367            month = 0
2368
2369        try:
2370            day = int(start[2])
2371        except:
2372            day = 0
2373
2374        try:
2375            rng_year = int(stop[0])
2376        except:
2377            rng_year = 0
2378
2379        try:
2380            rng_month = int(stop[1])
2381        except:
2382            rng_month = 0
2383
2384        try:
2385            rng_day = int(stop[2])
2386        except:
2387            rng_day = 0
2388
2389        if "cformat" in attrs:
2390            cal = Date.calendar_names.index(attrs['cformat'])
2391        else:
2392            cal = Date.CAL_GREGORIAN
2393
2394        if 'quality' in attrs:
2395            val = attrs['quality']
2396            if val == 'estimated':
2397                qual = Date.QUAL_ESTIMATED
2398            elif val == 'calculated':
2399                qual = Date.QUAL_CALCULATED
2400            else:
2401                qual = Date.QUAL_NONE
2402        else:
2403            qual = Date.QUAL_NONE
2404
2405        dualdated = False
2406        if 'dualdated' in attrs:
2407            val = attrs['dualdated']
2408            if val == "1":
2409                dualdated = True
2410
2411        newyear = Date.NEWYEAR_JAN1
2412        if 'newyear' in attrs:
2413            newyear = attrs['newyear']
2414            if newyear.isdigit():
2415                newyear = int(newyear)
2416            else:
2417                newyear = Date.newyear_to_code(newyear)
2418
2419        try:
2420            date_value.set(qual, mode, cal,
2421                           (day, month, year, dualdated,
2422                            rng_day, rng_month, rng_year, dualdated),
2423                           newyear=newyear)
2424        except DateError as e:
2425            self._set_date_to_xml_text(date_value, e,
2426                    xml_element_name = ("datespan" if mode == Date.MOD_SPAN
2427                        else "daterange"),
2428                    xml_attrs = attrs)
2429
2430    def start_dateval(self, attrs):
2431        if self.citation:
2432            date_value = self.citation.get_date_object()
2433        elif self.ord:
2434            date_value = self.ord.get_date_object()
2435        elif self.object:
2436            date_value = self.object.get_date_object()
2437        elif self.address:
2438            date_value = self.address.get_date_object()
2439        elif self.name:
2440            date_value = self.name.get_date_object()
2441        elif self.event:
2442            date_value = self.event.get_date_object()
2443        elif self.placeref:
2444            date_value = self.placeref.get_date_object()
2445        elif self.place_name:
2446            date_value = self.place_name.get_date_object()
2447
2448        bce = 1
2449        val = attrs['val']
2450        if val[0] == '-':
2451            bce = -1
2452            val = val[1:]
2453        start = val.split('-')
2454        try:
2455            year = int(start[0])*bce
2456        except:
2457            year = 0
2458
2459        try:
2460            month = int(start[1])
2461        except:
2462            month = 0
2463
2464        try:
2465            day = int(start[2])
2466        except:
2467            day = 0
2468
2469        if "cformat" in attrs:
2470            cal = Date.calendar_names.index(attrs['cformat'])
2471        else:
2472            cal = Date.CAL_GREGORIAN
2473
2474        if 'type' in attrs:
2475            val = attrs['type']
2476            if val == "about":
2477                mod = Date.MOD_ABOUT
2478            elif val == "after":
2479                mod = Date.MOD_AFTER
2480            else:
2481                mod = Date.MOD_BEFORE
2482        else:
2483            mod = Date.MOD_NONE
2484
2485        if 'quality' in attrs:
2486            val = attrs['quality']
2487            if val == 'estimated':
2488                qual = Date.QUAL_ESTIMATED
2489            elif val == 'calculated':
2490                qual = Date.QUAL_CALCULATED
2491            else:
2492                qual = Date.QUAL_NONE
2493        else:
2494            qual = Date.QUAL_NONE
2495
2496        dualdated = False
2497        if 'dualdated' in attrs:
2498            val = attrs['dualdated']
2499            if val == "1":
2500                dualdated = True
2501
2502        newyear = Date.NEWYEAR_JAN1
2503        if 'newyear' in attrs:
2504            newyear = attrs['newyear']
2505            if newyear.isdigit():
2506                newyear = int(newyear)
2507            else:
2508                newyear = Date.newyear_to_code(newyear)
2509
2510        try:
2511            date_value.set(qual, mod, cal, (day, month, year, dualdated),
2512                           newyear=newyear)
2513        except DateError as e:
2514            self._set_date_to_xml_text(date_value, e, 'dateval', attrs)
2515
2516    def _set_date_to_xml_text(self, date_value, date_error, xml_element_name, xml_attrs):
2517        """
2518        Common handling of invalid dates for the date... element handlers.
2519
2520        Prints warning on console and sets date_value to a text-only date
2521        with the problematic XML inside.
2522        """
2523        xml = "<{element_name} {attrs}/>".format(
2524            element_name = xml_element_name,
2525            attrs = " ".join(
2526                ['{}="{}"'.format(k,escape(v, entities={'"' : "&quot;"}))
2527                    for k,v in xml_attrs.items()]))
2528        # TRANSLATORS: leave the {date} and {xml} untranslated in the format string,
2529        # but you may re-order them if needed.
2530        LOG.warning(_("Invalid date {date} in XML {xml}, preserving XML as text"
2531            ).format(date=date_error.date.__dict__, xml=xml))
2532        date_value.set(modifier=Date.MOD_TEXTONLY, text=xml)
2533
2534    def start_datestr(self, attrs):
2535        if self.citation:
2536            date_value = self.citation.get_date_object()
2537        elif self.ord:
2538            date_value = self.ord.get_date_object()
2539        elif self.object:
2540            date_value = self.object.get_date_object()
2541        elif self.address:
2542            date_value = self.address.get_date_object()
2543        elif self.name:
2544            date_value = self.name.get_date_object()
2545        elif self.event:
2546            date_value = self.event.get_date_object()
2547        elif self.placeref:
2548            date_value = self.placeref.get_date_object()
2549        else:
2550            date_value = self.place_name.get_date_object()
2551
2552        date_value.set_as_text(attrs['val'])
2553
2554    def start_pos(self, attrs):
2555        self.person.position = (int(attrs["x"]), int(attrs["y"]))
2556
2557    def stop_attribute(self, *tag):
2558        self.attribute = None
2559
2560    def stop_srcattribute(self, *tag):
2561        self.srcattribute = None
2562
2563    def stop_comment(self, tag):
2564        # Parse witnesses created by older gramps
2565        if tag.strip():
2566            self.witness_comment = tag
2567        else:
2568            self.witness_comment = ""
2569
2570    def stop_witness(self, tag):
2571        # Parse witnesses created by older gramps
2572        if self.witness_comment:
2573            text = self.witness_comment
2574        elif tag.strip():
2575            text = tag
2576        else:
2577            text = None
2578
2579        if text is not None:
2580            note = Note()
2581            note.handle = create_id()
2582            note.set(_("Witness comment: %s") % text)
2583            note.type.set(NoteType.EVENT)
2584            note.private = self.event.private
2585            self.db.add_note(note, self.trans)
2586            #set correct change time
2587            self.db.commit_note(note, self.trans, self.change)
2588            self.info.add('new-object', NOTE_KEY, note)
2589            self.event.add_note(note.handle)
2590        self.in_witness = False
2591
2592    def stop_attr_type(self, tag):
2593        self.attribute.set_type(tag)
2594
2595    def stop_attr_value(self, tag):
2596        self.attribute.set_value(tag)
2597
2598    def stop_address(self, *tag):
2599        if self.person:
2600            self.person.add_address(self.address)
2601        elif self.repo:
2602            self.repo.add_address(self.address)
2603        self.address = None
2604
2605    def stop_places(self, *tag):
2606        self.placeobj = None
2607
2608        if self.__xml_version < (1, 6, 0):
2609            self.place_import.generate_hierarchy(self.trans)
2610
2611    def stop_photo(self, *tag):
2612        self.photo = None
2613
2614    def stop_ptitle(self, tag):
2615        self.placeobj.title = tag
2616
2617    def stop_code(self, tag):
2618        self.placeobj.code = tag
2619
2620    def stop_alt_name(self, tag):
2621        place_name = PlaceName()
2622        place_name.set_value(tag)
2623        self.placeobj.add_alternative_name(place_name)
2624
2625    def stop_placeobj(self, *tag):
2626        if self.placeobj.name.get_value() == '':
2627            self.placeobj.name.set_value(self.placeobj.title)
2628        self.db.commit_place(self.placeobj, self.trans,
2629                             self.placeobj.get_change_time())
2630        self.placeobj = None
2631
2632    def stop_family(self, *tag):
2633        self.db.commit_family(self.family, self.trans,
2634                              self.family.get_change_time())
2635        self.family = None
2636
2637    def stop_type(self, tag):
2638        if self.event:
2639            # Event type
2640            self.event.type.set_from_xml_str(tag)
2641        elif self.repo:
2642            # Repository type
2643            self.repo.type.set_from_xml_str(tag)
2644
2645    def stop_childref(self, tag):
2646        self.childref = None
2647
2648    def stop_personref(self, tag):
2649        self.personref = None
2650
2651    def stop_eventref(self, tag):
2652        self.eventref = None
2653
2654    def stop_placeref(self, tag):
2655        self.placeref = None
2656
2657    def stop_event(self, *tag):
2658        if self.family:
2659            ref = EventRef()
2660            ref.ref = self.event.handle
2661            ref.private = self.event.private
2662            ref.role.set(EventRoleType.FAMILY)
2663            self.family.add_event_ref(ref)
2664        elif self.person:
2665            ref = EventRef()
2666            ref.ref = self.event.handle
2667            ref.private = self.event.private
2668            ref.role.set(EventRoleType.PRIMARY)
2669            if (self.event.type == EventType.BIRTH) \
2670                   and (self.person.get_birth_ref() is None):
2671                self.person.set_birth_ref(ref)
2672            elif (self.event.type == EventType.DEATH) \
2673                     and (self.person.get_death_ref() is None):
2674                self.person.set_death_ref(ref)
2675            else:
2676                self.person.add_event_ref(ref)
2677
2678        if self.event.get_description() == "" and \
2679               self.event.get_type() != EventType.CUSTOM:
2680            if self.family:
2681                text = EVENT_FAMILY_STR % {
2682                    'event_name' : str(self.event.get_type()),
2683                    'family' : family_name(self.family, self.db),
2684                    }
2685            elif self.person:
2686                text = EVENT_PERSON_STR % {
2687                    'event_name' : str(self.event.get_type()),
2688                    'person' : name_displayer.display(self.person),
2689                    }
2690            else:
2691                text = ''
2692            self.event.set_description(text)
2693
2694        self.db.commit_event(self.event, self.trans,
2695                             self.event.get_change_time())
2696        self.event = None
2697
2698    def stop_name(self, attrs):
2699        if self.person:
2700            self.stop_person_name(attrs)
2701        if self.placeobj: # XML 1.7.0
2702            self.stop_place_name(attrs)
2703
2704    def stop_place_name(self, tag):
2705        self.place_name = None
2706
2707    def stop_person_name(self, tag):
2708        if self.in_witness:
2709            # Parse witnesses created by older gramps
2710            note = Note()
2711            note.handle = create_id()
2712            note.set(_("Witness name: %s") % tag)
2713            note.type.set(NoteType.EVENT)
2714            note.private = self.event.private
2715            self.db.add_note(note, self.trans)
2716            #set correct change time
2717            self.db.commit_note(note, self.trans, self.change)
2718            self.info.add('new-object', NOTE_KEY, note)
2719            self.event.add_note(note.handle)
2720        else:
2721            #first correct old xml that has no nametype set
2722            if self.alt_name:
2723                # alternate name or former aka tag
2724                if self.name.get_type() == "":
2725                    self.name.set_type(NameType.AKA)
2726            else:
2727                if self.name.get_type() == "":
2728                    self.name.set_type(NameType.BIRTH)
2729
2730            #same logic as bsddb upgrade for xml < 1.4.0 which will
2731            #have a surnamepat and/or surname. From 1.4.0 surname has been
2732            #added to name in self.stop_surname
2733            if not self.surnamepat:
2734                #no patronymic, only add surname if present
2735                if self.surname:
2736                    self.name.add_surname(self.surname)
2737                    self.name.set_primary_surname(0)
2738            else:
2739                #a patronymic, if no surname, a single surname
2740                if not self.surname:
2741                    self.name.add_surname(self.surnamepat)
2742                    self.name.set_primary_surname(0)
2743                else:
2744                    #two surnames, first patronymic, then surname which is primary
2745                    self.name.add_surname(self.surnamepat)
2746                    self.name.add_surname(self.surname)
2747                    self.name.set_primary_surname(1)
2748            if self.alt_name:
2749                self.person.add_alternate_name(self.name)
2750            else:
2751                self.person.set_primary_name(self.name)
2752        self.name = None
2753        self.surname = None
2754        self.surnamepat = None
2755
2756    def stop_aka(self, tag):
2757        if self.name.get_type() == "":
2758            self.name.set_type(NameType.AKA)
2759        if not self.surnamepat:
2760            #no patronymic, only add surname if present
2761            if self.surname:
2762                self.name.add_surname(self.surname)
2763                self.name.set_primary_surname(0)
2764        else:
2765            #a patronymic, if no surname, a single surname
2766            if not self.surname:
2767                self.name.add_surname(self.surnamepat)
2768                self.name.set_primary_surname(0)
2769            else:
2770                #two surnames, first patronymic, then surname which is primary
2771                self.name.add_surname(self.surnamepat)
2772                self.name.add_surname(self.surname)
2773                self.name.set_primary_surname(1)
2774        self.person.add_alternate_name(self.name)
2775        self.name = None
2776
2777    def stop_rname(self, tag):
2778        # Repository name
2779        self.repo.name = tag
2780
2781    def stop_ref(self, tag):
2782        """
2783        Parse witnesses created by older gramps
2784        """
2785        person = Person()
2786        self.inaugurate_id(tag, PERSON_KEY, person)
2787        # Add an EventRef from that person
2788        # to this event using ROLE_WITNESS role
2789        event_ref = EventRef()
2790        event_ref.ref = self.event.handle
2791        event_ref.role.set(EventRoleType.WITNESS)
2792        person.event_ref_list.append(event_ref)
2793        self.db.commit_person(person, self.trans, self.change)
2794
2795    def stop_place(self, tag):
2796        """end of a reference to place, should do nothing ...
2797           Note, if we encounter <place>blabla</place> this method is called
2798                with tag='blabla
2799        """
2800        ##place = None
2801        ##handle = None
2802        ##if self.place_ref is None:  #todo, add place_ref in start and init
2803        ##    #legacy cody? I see no reason for this, but it was present
2804        ##    if tag in self.place_map:
2805        ##        place = self.place_map[tag]
2806        ##        handle = place.get_handle()
2807        ##        place = None
2808        ##    else:
2809        ##        place = RelLib.Place()
2810        ##        place.set_title(tag)
2811        ##        handle = place.get_handle()
2812        ##    if self.ord:
2813        ##        self.ord.set_place_handle(handle)
2814        ##    elif self.object:
2815        ##        self.object.set_place_handle(handle)
2816        ##    else:
2817        ##        self.event.set_place_handle(handle)
2818        ##    if place :
2819        ##        self.db.commit_place(self.placeobj,self.trans,self.change)
2820        ##self.place_ref = None
2821        pass
2822
2823    def stop_date(self, tag):
2824        if tag:
2825            if self.address:
2826                set_date(self.address, tag)
2827            else:
2828                set_date(self.event, tag)
2829
2830    def stop_first(self, tag):
2831        # bug 9242
2832        if len(tag.splitlines()) != 1:
2833            tag = "".join(tag.splitlines())
2834        self.name.set_first_name(tag)
2835
2836    def stop_call(self, tag):
2837        self.name.set_call_name(tag)
2838
2839    def stop_families(self, *tag):
2840        self.family = None
2841
2842    def stop_person(self, *tag):
2843        self.db.commit_person(self.person, self.trans,
2844                              self.person.get_change_time())
2845        self.person = None
2846
2847    def stop_description(self, tag):
2848        self.event.set_description(tag)
2849
2850    def stop_cause(self, tag):
2851        # The old event's cause is now an attribute
2852        attr = Attribute()
2853        attr.set_type(AttributeType.CAUSE)
2854        attr.set_value(tag)
2855        self.event.add_attribute(attr)
2856
2857    def stop_gender(self, tag):
2858        t = tag
2859        if t == "M":
2860            self.person.set_gender (Person.MALE)
2861        elif t == "F":
2862            self.person.set_gender (Person.FEMALE)
2863        else:
2864            self.person.set_gender (Person.UNKNOWN)
2865
2866    def stop_stitle(self, tag):
2867        self.source.title = tag
2868
2869    def stop_sourceref(self, *tag):
2870        # if we are in an old style sourceref we need to commit the citation
2871        if self.in_old_sourceref:
2872            self.db.commit_citation(self.citation, self.trans,
2873                                    self.citation.get_change_time())
2874            self.citation = None
2875            self.in_old_sourceref = False
2876
2877    def stop_source(self, *tag):
2878        self.db.commit_source(self.source, self.trans,
2879                              self.source.get_change_time())
2880        self.source = None
2881
2882    def stop_citation(self, *tag):
2883        self.db.commit_citation(self.citation, self.trans,
2884                                self.citation.get_change_time())
2885        self.citation = None
2886
2887    def stop_sauthor(self, tag):
2888        self.source.author = tag
2889
2890    def stop_phone(self, tag):
2891        self.address.phone = tag
2892
2893    def stop_street(self, tag):
2894        self.address.street = tag
2895
2896    def stop_locality(self, tag):
2897        self.address.locality = tag
2898
2899    def stop_city(self, tag):
2900        self.address.city = tag
2901
2902    def stop_county(self, tag):
2903        self.address.county = tag
2904
2905    def stop_state(self, tag):
2906        self.address.state = tag
2907
2908    def stop_country(self, tag):
2909        self.address.country = tag
2910
2911    def stop_postal(self, tag):
2912        self.address.set_postal_code(tag)
2913
2914    def stop_spage(self, tag):
2915        # Valid for version <= 1.4.0
2916        self.citation.set_page(tag)
2917
2918    def stop_page(self, tag):
2919        # Valid for version >= 1.5.0
2920        self.citation.set_page(tag)
2921
2922    def stop_confidence(self, tag):
2923        # Valid for version >= 1.5.0
2924        self.citation.set_confidence_level(int(tag))
2925
2926    def stop_lds_ord(self, *tag):
2927        self.ord = None
2928
2929    def stop_spubinfo(self, tag):
2930        self.source.set_publication_info(tag)
2931
2932    def stop_sabbrev(self, tag):
2933        self.source.set_abbreviation(tag)
2934
2935    def stop_stext(self, tag):
2936        if self.use_p:
2937            self.use_p = 0
2938            text = fix_spaces(self.stext_list)
2939        else:
2940            text = tag
2941        # This is old XML. We no longer have "text" attribute in soure_ref.
2942        # So we create a new note, commit, and add the handle to note list.
2943        note = Note()
2944        note.handle = create_id()
2945        note.private = self.citation.private
2946        note.set(text)
2947        note.type.set(NoteType.SOURCE_TEXT)
2948        self.db.add_note(note, self.trans)
2949        #set correct change time
2950        self.db.commit_note(note, self.trans, self.change)
2951        self.info.add('new-object', NOTE_KEY, note)
2952        self.citation.add_note(note.handle)
2953
2954    def stop_scomments(self, tag):
2955        if self.use_p:
2956            self.use_p = 0
2957            text = fix_spaces(self.scomments_list)
2958        else:
2959            text = tag
2960        note = Note()
2961        note.handle = create_id()
2962        note.private = self.citation.private
2963        note.set(text)
2964        note.type.set(NoteType.CITATION)
2965        self.db.add_note(note, self.trans)
2966        #set correct change time
2967        self.db.commit_note(note, self.trans, self.change)
2968        self.info.add('new-object', NOTE_KEY, note)
2969        self.citation.add_note(note.handle)
2970
2971    def stop_last(self, tag):
2972        if self.surname:
2973            self.surname.set_surname(tag)
2974        if not tag.strip() and not self.surname.get_prefix().strip():
2975            #consider empty surname as no surname
2976            self.surname = None
2977
2978    def stop_surname(self, tag):
2979        """Add surname to name, validating only one primary."""
2980        if self.name:
2981            self.surname.set_surname(tag)
2982            if any(sname.get_primary() for sname in self.name.get_surname_list()):
2983                self.surname.set_primary(False)
2984            self.name.add_surname(self.surname)
2985        self.surname = None
2986
2987    def stop_group(self, tag):
2988        """ group name of a name"""
2989        if self.name:
2990            self.name.set_group_as(tag)
2991
2992    def stop_suffix(self, tag):
2993        if self.name:
2994            self.name.set_suffix(tag)
2995
2996    def stop_patronymic(self, tag):
2997        if self.surnamepat:
2998            self.surnamepat.set_surname(tag)
2999        if not tag.strip():
3000            self.surnamepat = None
3001
3002    def stop_title(self, tag):
3003        if self.name:
3004            self.name.set_title(tag)
3005
3006    def stop_nick(self, tag):
3007        """in < 1.3.0 nick is on person and mapped to attribute
3008           from 1.4.0 it is a name element
3009        """
3010        if self.name:
3011            self.name.set_nick_name(tag)
3012        elif self.person:
3013            attr = Attribute()
3014            attr.set_type(AttributeType.NICKNAME)
3015            attr.set_value(tag)
3016            self.person.add_attribute(attr)
3017
3018    def stop_familynick(self, tag):
3019        if self.name:
3020            self.name.set_family_nick_name(tag)
3021
3022    def stop_text(self, tag):
3023        self.note_text = tag
3024
3025    def stop_note(self, tag):
3026        self.in_note = 0
3027        if self.use_p:
3028            self.use_p = 0
3029            text = fix_spaces(self.note_list)
3030        elif self.note_text is not None:
3031            text = self.note_text
3032        else:
3033            text = tag
3034
3035        self.note.set_styledtext(StyledText(text, self.note_tags))
3036
3037        # The order in this long if-then statement should reflect the
3038        # DTD: most deeply nested elements come first.
3039        if self.address:
3040            self.address.add_note(self.note.handle)
3041        elif self.ord:
3042            self.ord.add_note(self.note.handle)
3043        elif self.attribute:
3044            self.attribute.add_note(self.note.handle)
3045        elif self.object:
3046            self.object.add_note(self.note.handle)
3047        elif self.objref:
3048            self.objref.add_note(self.note.handle)
3049        elif self.photo:
3050            self.photo.add_note(self.note.handle)
3051        elif self.name:
3052            self.name.add_note(self.note.handle)
3053        elif self.eventref:
3054            self.eventref.add_note(self.note.handle)
3055        elif self.reporef:
3056            self.reporef.add_note(self.note.handle)
3057        elif self.source:
3058            self.source.add_note(self.note.handle)
3059        elif self.event:
3060            self.event.add_note(self.note.handle)
3061        elif self.personref:
3062            self.personref.add_note(self.note.handle)
3063        elif self.person:
3064            self.person.add_note(self.note.handle)
3065        elif self.childref:
3066            self.childref.add_note(self.note.handle)
3067        elif self.family:
3068            self.family.add_note(self.note.handle)
3069        elif self.placeobj:
3070            self.placeobj.add_note(self.note.handle)
3071        elif self.repo:
3072            self.repo.add_note(self.note.handle)
3073
3074        self.db.commit_note(self.note, self.trans, self.note.get_change_time())
3075        self.note = None
3076
3077    def stop_note_asothers(self, *tag):
3078        self.db.commit_note(self.note, self.trans, self.note.get_change_time())
3079        self.note = None
3080
3081    def stop_research(self, tag):
3082        self.owner.set_name(self.resname)
3083        self.owner.set_address(self.resaddr)
3084        self.owner.set_locality(self.reslocality)
3085        self.owner.set_city(self.rescity)
3086        self.owner.set_state(self.resstate)
3087        self.owner.set_country(self.rescon)
3088        self.owner.set_postal_code(self.respos)
3089        self.owner.set_phone(self.resphone)
3090        self.owner.set_email(self.resemail)
3091
3092    def stop_resname(self, tag):
3093        self.resname = tag
3094
3095    def stop_resaddr(self, tag):
3096        self.resaddr = tag
3097
3098    def stop_reslocality(self, tag):
3099        self.reslocality = tag
3100
3101    def stop_rescity(self, tag):
3102        self.rescity = tag
3103
3104    def stop_resstate(self, tag):
3105        self.resstate = tag
3106
3107    def stop_rescountry(self, tag):
3108        self.rescon = tag
3109
3110    def stop_respostal(self, tag):
3111        self.respos = tag
3112
3113    def stop_resphone(self, tag):
3114        self.resphone = tag
3115
3116    def stop_resemail(self, tag):
3117        self.resemail = tag
3118
3119    def stop_mediapath(self, tag):
3120        self.mediapath = tag
3121
3122    def stop_ptag(self, tag):
3123        self.use_p = 1
3124        if self.in_note:
3125            self.note_list.append(tag)
3126        elif self.in_stext:
3127            self.stext_list.append(tag)
3128        elif self.in_scomments:
3129            self.scomments_list.append(tag)
3130
3131    def startElement(self, tag, attrs):
3132        self.func_list[self.func_index] = (self.func, self.tlist)
3133        self.func_index += 1
3134        self.tlist = []
3135
3136        try:
3137            f, self.func = self.func_map[tag]
3138            if f:
3139                f(attrs)
3140        except KeyError:
3141            self.func_map[tag] = (None, None)
3142            self.func = None
3143
3144    def endElement(self, tag):
3145        if self.func:
3146            self.func(''.join(self.tlist))
3147        self.func_index -= 1
3148        self.func, self.tlist = self.func_list[self.func_index]
3149
3150    def characters(self, data):
3151        if self.func:
3152            self.tlist.append(data)
3153
3154    def convert_marker(self, attrs, obj):
3155        """
3156        Convert markers into tags.
3157
3158        Old and new markers: complete=1 and marker=word
3159        """
3160        if attrs.get('complete'): # this is only true for complete=1
3161            tag_name = 'Complete'
3162        else:
3163            tag_name = attrs.get('marker')
3164
3165        if tag_name is not None:
3166            tag_name = _(tag_name)
3167            tag = self.db.get_tag_from_name(tag_name)
3168            if tag is None:
3169                tag = Tag()
3170                tag.set_name(tag_name)
3171                tag.set_priority(self.db.get_number_of_tags())
3172                tag_handle = self.db.add_tag(tag, self.trans)
3173            else:
3174                tag_handle = tag.get_handle()
3175            obj.add_tag(tag_handle)
3176
3177    def fix_not_instantiated(self):
3178        uninstantiated = []
3179        for orig_handle in self.import_handles.keys():
3180            tglist = [target for target in self.import_handles[orig_handle].keys() if
3181                    not self.import_handles[orig_handle][target][INSTANTIATED]]
3182            for target in tglist:
3183                uninstantiated += [(orig_handle, target)]
3184        if uninstantiated:
3185            expl_note = create_explanation_note(self.db)
3186            self.db.commit_note(expl_note, self.trans, time.time())
3187            self.info.expl_note = expl_note.get_gramps_id()
3188            for orig_handle, target in uninstantiated:
3189                class_arg = {'handle': orig_handle, 'id': None, 'priv': False}
3190                if target == 'family':
3191                    objs = make_unknown(class_arg, expl_note.handle,
3192                            self.func_map[target][0], self.func_map[target][1],
3193                            self.trans, db=self.db)
3194                elif target == 'citation':
3195                    objs = make_unknown(class_arg, expl_note.handle,
3196                            self.func_map[target][0], self.func_map[target][1],
3197                            self.trans,
3198                            source_class_func=self.func_map['source'][0],
3199                            source_commit_func=self.func_map['source'][1],
3200                            source_class_arg={'handle':create_id(), 'id':None, 'priv':False})
3201                elif target == 'note':
3202                    objs = make_unknown(class_arg, expl_note.handle,
3203                            self.func_map[target][0], self.stop_note_asothers,
3204                            self.trans)
3205                else:
3206                    if target == 'place':
3207                        target = 'placeobj'
3208                    elif target == 'media':
3209                        target = 'object'
3210                    objs = make_unknown(class_arg, expl_note.handle,
3211                            self.func_map[target][0], self.func_map[target][1],
3212                            self.trans)
3213                for obj in objs:
3214                    key = CLASS_TO_KEY_MAP[obj.__class__.__name__]
3215                    self.info.add('unknown-object', key, obj)
3216
3217    def fix_families(self):
3218        # Fix any imported families where there is a link from the family to an
3219        # individual, but no corresponding link from the individual to the
3220        # family.
3221        for orig_handle in list(self.import_handles.keys()):
3222            for target in list(self.import_handles[orig_handle].keys()):
3223                if target == 'family':
3224                    family_handle = self.import_handles[orig_handle][target][HANDLE]
3225                    family = self.db.get_family_from_handle(family_handle)
3226                    father_handle = family.get_father_handle()
3227                    mother_handle = family.get_mother_handle()
3228
3229                    if father_handle:
3230                        father = self.db.get_person_from_handle(father_handle)
3231                        if father and \
3232                            family_handle not in father.get_family_handle_list():
3233                            father.add_family_handle(family_handle)
3234                            self.db.commit_person(father, self.trans)
3235                            txt = _("Error: family '%(family)s'"
3236                                           " father '%(father)s'"
3237                                           " does not refer"
3238                                           " back to the family."
3239                                           " Reference added." %
3240                                           {'family' : family.gramps_id,
3241                                            'father' : father.gramps_id})
3242                            self.info.add('unlinked-family', txt, None)
3243                            LOG.warning(txt)
3244
3245                    if mother_handle:
3246                        mother = self.db.get_person_from_handle(mother_handle)
3247                        if mother and \
3248                            family_handle not in mother.get_family_handle_list():
3249                            mother.add_family_handle(family_handle)
3250                            self.db.commit_person(mother, self.trans)
3251                            txt = _("Error: family '%(family)s'"
3252                                           " mother '%(mother)s'"
3253                                           " does not refer"
3254                                           " back to the family."
3255                                           " Reference added." %
3256                                           {'family' : family.gramps_id,
3257                                            'mother' : mother.gramps_id})
3258                            self.info.add('unlinked-family', txt, None)
3259                            LOG.warning(txt)
3260
3261                    for child_ref in family.get_child_ref_list():
3262                        child_handle = child_ref.ref
3263                        child = self.db.get_person_from_handle(child_handle)
3264                        if child:
3265                            if family_handle not in \
3266                                child.get_parent_family_handle_list():
3267                                # The referenced child has no reference to the
3268                                # family. There was a link from the FAM record
3269                                # to the child, but no FAMC link from the child
3270                                # to the FAM.
3271                                child.add_parent_family_handle(family_handle)
3272                                self.db.commit_person(child, self.trans)
3273                                txt = _("Error: family '%(family)s'"
3274                                               " child '%(child)s'"
3275                                               " does not "
3276                                               "refer back to the family. "
3277                                               "Reference added." %
3278                                               {'family' : family.gramps_id,
3279                                                'child' : child.gramps_id})
3280                                self.info.add('unlinked-family', txt, None)
3281                                LOG.warning(txt)
3282
3283def append_value(orig, val):
3284    if orig:
3285        return "%s, %s" % (orig, val)
3286    else:
3287        return val
3288
3289def build_place_title(loc):
3290    "Builds a title from a location"
3291    value = ""
3292    if loc.parish:
3293        value = loc.parish
3294    if loc.city:
3295        value = append_value(value, loc.city)
3296    if loc.county:
3297        value = append_value(value, loc.county)
3298    if loc.state:
3299        value = append_value(value, loc.state)
3300    if loc.country:
3301        value = append_value(value, loc.country)
3302    return value
3303
3304