示例#1
0
    def add(self, *obj):
        person = Person()

        # attempt to get the current surname
        (model, pathlist) = self.selection.get_selected_rows()
        name = Name()
        #the editor requires a surname
        name.add_surname(Surname())
        name.set_primary_surname(0)
        basepers = None
        if len(pathlist) == 1:
            path = pathlist[0]
            pathids = path.get_indices()
            if len(pathids) == 1:
                path = Gtk.TreePath((pathids[0], 0))
            iter_ = model.get_iter(path)
            handle = model.get_handle_from_iter(iter_)
            basepers = self.dbstate.db.get_person_from_handle(handle)
        if basepers:
            preset_name(basepers, name)
        person.set_primary_name(name)
        try:
            EditPerson(self.dbstate, self.uistate, [], person)
        except WindowActiveError:
            pass
示例#2
0
 def add_mother_clicked(self, obj):
     person = Person()
     person.set_gender(Person.FEMALE)
     autoname = config.get("behavior.surname-guessing")
     # _("Father's surname"),
     # _("None"),
     # _("Combination of mother's and father's surname"),
     # _("Icelandic style"),
     if autoname == 2:
         name = self.latin_american_child("mother")
     else:
         name = self.no_name()
     person.set_primary_name(name)
     EditPerson(self.dbstate, self.uistate, self.track, person, self.new_mother_added)
示例#3
0
 def add_mother_clicked(self, obj):
     person = Person()
     person.set_gender(Person.FEMALE)
     autoname = config.get('behavior.surname-guessing')
     #_("Father's surname"),
     #_("None"),
     #_("Combination of mother's and father's surname"),
     #_("Icelandic style"),
     if autoname == 2:
         name = self.latin_american_child("mother")
     else:
         name = self.no_name()
     person.set_primary_name(name)
     EditPerson(self.dbstate, self.uistate, self.track, person,
                self.new_mother_added)
示例#4
0
    def add_button_clicked(self, obj=None):
        person = Person()
        autoname = config.get("behavior.surname-guessing")
        # _("Father's surname"),
        # _("None"),
        # _("Combination of mother's and father's surname"),
        # _("Icelandic style"),
        if autoname == 0:
            name = self.north_american()
        elif autoname == 2:
            name = self.latin_american()
        else:
            name = self.no_name()
        person.set_primary_name(name)

        EditPerson(self.dbstate, self.uistate, self.track, person, self.new_child_added)
示例#5
0
    def add_button_clicked(self, obj=None):
        person = Person()
        autoname = config.get('behavior.surname-guessing')
        #_("Father's surname"),
        #_("None"),
        #_("Combination of mother's and father's surname"),
        #_("Icelandic style"),
        if autoname == 0:
            name = self.north_american()
        elif autoname == 2:
            name = self.latin_american()
        else:
            name = self.no_name()
        person.set_primary_name(name)

        EditPerson(self.dbstate, self.uistate, self.track, person,
                   self.new_child_added)
示例#6
0
 def add_child_to_fam(self, obj, event, handle):
     if button_activated(event, _LEFT_BUTTON):
         callback = lambda x: self.callback_add_child(x, handle)
         person = Person()
         name = Name()
         #the editor requires a surname
         name.add_surname(Surname())
         name.set_primary_surname(0)
         family = self.dbstate.db.get_family_from_handle(handle)
         father = self.dbstate.db.get_person_from_handle(
                                     family.get_father_handle())
         if father:
             preset_name(father, name)
         person.set_primary_name(name)
         try:
             EditPerson(self.dbstate, self.uistate, [], person,
                        callback=callback)
         except WindowActiveError:
             pass
示例#7
0
    def run_tool(self):
        self.progress = ProgressMeter(_('Running Date Test'),'',
                                        parent=self.parent_window)
        self.progress.set_pass(_('Generating dates'),
                               4)
        dates = []
        # first some valid dates
        calendar = Date.CAL_GREGORIAN
        for quality in (Date.QUAL_NONE, Date.QUAL_ESTIMATED,
                        Date.QUAL_CALCULATED):
            for modifier in (Date.MOD_NONE, Date.MOD_BEFORE,
                             Date.MOD_AFTER, Date.MOD_ABOUT):
                for slash1 in (False,True):
                    for month in range(0,13):
                        for day in (0,5,27):
                            if not month and day:
                                continue
                            d = Date()
                            d.set(quality,modifier,calendar,(day,month,1789,slash1),"Text comment")
                            dates.append( d)
            for modifier in (Date.MOD_RANGE, Date.MOD_SPAN):
                for slash1 in (False,True):
                    for slash2 in (False,True):
                        for month in range(0,13):
                            for day in (0,5,27):
                                if not month and day:
                                    continue

                                d = Date()
                                d.set(quality,modifier,calendar,(day,month,1789,slash1,day,month,1876,slash2),"Text comment")
                                dates.append( d)

                                if not month:
                                    continue

                                d = Date()
                                d.set(quality,modifier,calendar,(day,month,1789,slash1,day,13-month,1876,slash2),"Text comment")
                                dates.append( d)

                                if not day:
                                    continue

                                d = Date()
                                d.set(quality,modifier,calendar,(day,month,1789,slash1,32-day,month,1876,slash2),"Text comment")
                                dates.append( d)
                                d = Date()
                                d.set(quality,modifier,calendar,(day,month,1789,slash1,32-day,13-month,1876,slash2),"Text comment")
                                dates.append( d)
            modifier = Date.MOD_TEXTONLY
            d = Date()
            d.set(quality,modifier,calendar,Date.EMPTY,
                  "This is a textual date")
            dates.append( d)
            self.progress.step()
        
        # test invalid dates
        #dateval = (4,7,1789,False,5,8,1876,False)
        #for l in range(1,len(dateval)):
        #    d = Date()
        #    try:
        #        d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #              Date.CAL_GREGORIAN,dateval[:l],"Text comment")
        #        dates.append( d)
        #    except DateError, e:
        #        d.set_as_text("Date identified value correctly as invalid.\n%s" % e)
        #        dates.append( d)
        #    except:
        #        d = Date()
        #        d.set_as_text("Date.set Exception %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
        #        dates.append( d)
        #for l in range(1,len(dateval)):
        #    d = Date()
        #    try:
        #        d.set(Date.QUAL_NONE,Date.MOD_SPAN,Date.CAL_GREGORIAN,dateval[:l],"Text comment")
        #        dates.append( d)
        #    except DateError, e:
        #        d.set_as_text("Date identified value correctly as invalid.\n%s" % e)
        #        dates.append( d)
        #    except:
        #        d = Date()
        #        d.set_as_text("Date.set Exception %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
        #        dates.append( d)
        #self.progress.step()
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #      Date.CAL_GREGORIAN,(44,7,1789,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #      Date.CAL_GREGORIAN,(4,77,1789,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_SPAN,
        #      Date.CAL_GREGORIAN,
        #      (4,7,1789,False,55,8,1876,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_SPAN,
        #      Date.CAL_GREGORIAN,
        #      (4,7,1789,False,5,88,1876,False),"Text comment")
        #dates.append( d)
        
        with DbTxn(_("Date Test Plugin"), self.db, batch=True) as self.trans:
            self.db.disable_signals()
            self.progress.set_pass(_('Generating dates'),
                                   len(dates))

            # create pass and fail tags
            pass_handle = self.create_tag(_('Pass'), '#0000FFFF0000')
            fail_handle = self.create_tag(_('Fail'), '#FFFF00000000')

            # now add them as birth to new persons
            i = 1
            for dateval in dates:
                person = Person()
                surname = Surname()
                surname.set_surname("DateTest")
                name = Name()
                name.add_surname(surname)
                name.set_first_name("Test %d" % i)
                person.set_primary_name(name)
                self.db.add_person(person, self.trans)
                bevent = Event()
                bevent.set_type(EventType.BIRTH)
                bevent.set_date_object(dateval)
                bevent.set_description("Date Test %d (source)" % i)
                bevent_h = self.db.add_event(bevent, self.trans)
                bevent_ref = EventRef()
                bevent_ref.set_reference_handle(bevent_h)
                # for the death event display the date as text and parse it back to a new date
                ndate = None
                try:
                    datestr = _dd.display( dateval)
                    try:
                        ndate = _dp.parse( datestr)
                        if not ndate:
                            ndate = Date()
                            ndate.set_as_text("DateParser None")
                            person.add_tag(fail_handle)
                        else:
                            person.add_tag(pass_handle)
                    except:
                        ndate = Date()
                        ndate.set_as_text("DateParser Exception %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
                        person.add_tag(fail_handle)
                    else:
                        person.add_tag(pass_handle)
                except:
                    ndate = Date()
                    ndate.set_as_text("DateDisplay Exception: %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
                    person.add_tag(fail_handle)
                
                if dateval.get_modifier() != Date.MOD_TEXTONLY \
                       and ndate.get_modifier() == Date.MOD_TEXTONLY:
                    # parser was unable to correctly parse the string
                    ndate.set_as_text( "TEXTONLY: "+ndate.get_text())
                    person.add_tag(fail_handle)
                if dateval.get_modifier() == Date.MOD_TEXTONLY \
                        and dateval.get_text().count("Traceback") \
                        and pass_handle in person.get_tag_list():
                    person.add_tag(fail_handle)
                
                devent = Event()
                devent.set_type(EventType.DEATH)
                devent.set_date_object(ndate)
                devent.set_description("Date Test %d (result)" % i)
                devent_h = self.db.add_event(devent, self.trans)
                devent_ref = EventRef()
                devent_ref.set_reference_handle(devent_h)
                person.set_birth_ref(bevent_ref)
                person.set_death_ref(devent_ref)
                self.db.commit_person(person, self.trans)
                i = i + 1
                self.progress.step()
        self.db.enable_signals()
        self.db.request_rebuild()
        self.progress.close()
示例#8
0
class VCardParser:
    """Class to read data in VCard format from a file."""
    DATE_RE = re.compile(r'^(\d{4}-\d{1,2}-\d{1,2})|(?:(\d{4})-?(\d\d)-?(\d\d))')
    GROUP_RE = re.compile(r'^(?:[-0-9A-Za-z]+\.)?(.+)$')  # see RFC 2425 sec5.8.2
    ESCAPE_CHAR = '\\'
    TOBE_ESCAPED = ['\\', ',', ';']  # order is important
    LINE_CONTINUATION = [' ', '\t']

    @staticmethod
    def name_value_split(data):
        """Property group.name:value split is on first unquoted colon."""
        colon_idx = data.find(':')
        if colon_idx < 1:
            return ()
        quote_count = data.count('"', 0, colon_idx)
        while quote_count % 2 == 1:
            colon_idx = data.find(':', colon_idx + 1)
            quote_count = data.count('"', 0, colon_idx)
        group_name, value = data[:colon_idx], data[colon_idx + 1:]
        name_parts = VCardParser.GROUP_RE.match(group_name)
        return (name_parts.group(1), value)

    @staticmethod
    def unesc(data):
        """Remove VCard escape sequences."""
        if type(data) == type('string'):
            for char in reversed(VCardParser.TOBE_ESCAPED):
                data = data.replace(VCardParser.ESCAPE_CHAR + char, char)
            return data
        elif type(data) == type([]):
            return list(map(VCardParser.unesc, data))
        else:
            raise TypeError("VCard unescaping is not implemented for "
                              "data type %s." % str(type(data)))

    @staticmethod
    def count_escapes(strng):
        """Count the number of escape characters at the end of a string."""
        count = 0
        for char in reversed(strng):
            if char != VCardParser.ESCAPE_CHAR:
                return count
            count += 1
        return count

    @staticmethod
    def split_unescaped(strng, sep):
        """Split on sep if sep is unescaped."""
        strng_parts = strng.split(sep)
        for i in reversed(range(len(strng_parts[:]))):
            if VCardParser.count_escapes(strng_parts[i]) % 2 == 1:
                # the sep was escaped so undo split
                appendix = strng_parts.pop(i + 1)
                strng_parts[i] += sep + appendix
        return strng_parts

    def __init__(self, dbase):
        self.database = dbase
        self.formatted_name = ''
        self.name_parts = ''
        self.next_line = None
        self.trans = None
        self.version = None
        self.person = None
        self.errors = []
        self.number_of_errors = 0

    def __get_next_line(self, filehandle):
        """
        Read and return the line with the next property of the VCard.

        Also if it spans multiple lines (RFC 2425 sec.5.8.1).
        """
        line = self.next_line
        self.next_line = filehandle.readline()
        self.line_num = self.line_num + 1
        while self.next_line and self.next_line[0] in self.LINE_CONTINUATION:
            line = line.rstrip("\n")
            # TODO perhaps next lines superflous because of rU open parameter?
            if len(line) > 0 and line[-1] == "\r":
                line = line[:-1]
            line += self.next_line[1:]
            self.next_line = filehandle.readline()
            self.line_num = self.line_num + 1
        if line:
            line = line.strip()
        else:
            line = None
        return line

    def __add_msg(self, problem, line=None):
        if problem != "":
            self.number_of_errors += 1
        if line:
            message = _("Line %(line)5d: %(prob)s\n") % {"line": line,
                                                         "prob": problem}
        else:
            message = problem + "\n"
        self.errors.append(message)

    def parse(self, filehandle, user):
        """
        Prepare the database and parse the input file.

        :param filehandle: open file handle positioned at start of the file
        """
        tym = time.time()
        self.person = None
        self.database.disable_signals()
        with DbTxn(_("vCard import"), self.database, batch=True) as self.trans:
            self._parse_vCard_file(filehandle)
        self.database.enable_signals()
        self.database.request_rebuild()
        tym = time.time() - tym
        # translators: leave all/any {...} untranslated
        msg = ngettext('Import Complete: {number_of} second',
                       'Import Complete: {number_of} seconds', tym
                      ).format(number_of=tym)
        LOG.debug(msg)
        if self.number_of_errors == 0:
            message = _("VCARD import report: No errors detected")
        else:
            message = _("VCARD import report: %s errors detected\n") % \
                self.number_of_errors
        if hasattr(user.uistate, 'window'):
            parent_window = user.uistate.window
        else:
            parent_window = None
        user.info(message, "".join(self.errors),
                  parent=parent_window, monospaced=True)

    def _parse_vCard_file(self, filehandle):
        """Read each line of the input file and act accordingly."""
        self.next_line = filehandle.readline()
        self.line_num = 1

        while True:
            line = self.__get_next_line(filehandle)
            if line is None:
                break
            if line == "":
                continue

            if line.find(":") == -1:
                continue
            line_parts = self.name_value_split(line)
            if not line_parts:
                continue

            # No check for escaped ; because only fields[0] is used.
            fields = line_parts[0].split(";")

            property_name = fields[0].upper()
            if property_name == "BEGIN":
                self.next_person()
            elif property_name == "END":
                self.finish_person()
            elif property_name == "VERSION":
                self.check_version(fields, line_parts[1])
            elif property_name == "FN":
                self.add_formatted_name(fields, line_parts[1])
            elif property_name == "N":
                self.add_name_parts(fields, line_parts[1])
            elif property_name == "NICKNAME":
                self.add_nicknames(fields, line_parts[1])
            elif property_name == "SORT-STRING":
                self.add_sortas(fields, line_parts[1])
            elif property_name == "ADR":
                self.add_address(fields, line_parts[1])
            elif property_name == "TEL":
                self.add_phone(fields, line_parts[1])
            elif property_name == "BDAY":
                self.add_birthday(fields, line_parts[1])
            elif property_name == "ROLE":
                self.add_occupation(fields, line_parts[1])
            elif property_name == "URL":
                self.add_url(fields, line_parts[1])
            elif property_name == "EMAIL":
                self.add_email(fields, line_parts[1])
            elif property_name == "X-GENDER" or property_name == "GENDER":
                # VCard 3.0 only has X-GENDER, GENDER is 4.0 syntax,
                # but we want to be robust here.
                self.add_gender(fields, line_parts[1])
            elif property_name == "PRODID":
                # Included cause VCards made by Gramps have this prop.
                pass
            else:
                self.__add_msg(_("Token >%(token)s< unknown. line skipped: %(line)s") %
                               {"token": (fields[0], line), "line": self.line_num - 1})

    def finish_person(self):
        """All info has been collected, write to database."""
        if self.person is not None:
            if self.add_name():
                self.database.add_person(self.person, self.trans)
        self.person = None

    def next_person(self):
        """A VCard for another person is started."""
        if self.person is not None:
            self.finish_person()
            self.__add_msg(_("BEGIN property not properly closed by END "
                           "property, Gramps can't cope with nested VCards."),
                           self.line_num - 1)
        self.person = Person()
        self.formatted_name = ''
        self.name_parts = ''

    def check_version(self, fields, data):
        """Check the version of the VCard, only version 3.0 is supported."""
        self.version = data
        if self.version != "3.0":
            raise GrampsImportError(_("Import of VCards version %s is "
                    "not supported by Gramps.") % self.version)

    def add_formatted_name(self, fields, data):
        """Read the FN property of a VCard."""
        if not self.formatted_name:
            self.formatted_name = self.unesc(str(data)).strip()

    def add_name_parts(self, fields, data):
        """Read the N property of a VCard."""
        if not self.name_parts:
            self.name_parts = data.strip()

    def add_name(self):
        """
        Add the name to the person.

        Returns True on success, False on failure.
        """
        if not self.name_parts.strip():
            self.__add_msg(_("VCard is malformed missing the compulsory N "
                           "property, so there is no name; skip it."),
                           self.line_num - 1)
            return False
        if not self.formatted_name:
            self.__add_msg(_("VCard is malformed missing the compulsory FN "
                           "property, get name from N alone."), self.line_num - 1)
        data_fields = self.split_unescaped(self.name_parts, ';')
        if len(data_fields) != 5:
            self.__add_msg(_("VCard is malformed wrong number of name "
                           "components."), self.line_num - 1)

        name = Name()
        name.set_type(NameType(NameType.BIRTH))

        if data_fields[0].strip():
            # assume first surname is primary
            for surname_str in self.split_unescaped(data_fields[0], ','):
                surname = Surname()
                prefix, sname = splitof_nameprefix(self.unesc(surname_str))
                surname.set_surname(sname.strip())
                surname.set_prefix(prefix.strip())
                name.add_surname(surname)
            name.set_primary_surname()

        if len(data_fields) > 1 and data_fields[1].strip():
            given_name = ' '.join(self.unesc(
                                  self.split_unescaped(data_fields[1], ',')))
        else:
            given_name = ''
        if len(data_fields) > 2 and data_fields[2].strip():
            additional_names = ' '.join(self.unesc(
                                     self.split_unescaped(data_fields[2], ',')))
        else:
            additional_names = ''
        self.add_firstname(given_name.strip(), additional_names.strip(), name)

        if len(data_fields) > 3 and data_fields[3].strip():
            name.set_title(' '.join(self.unesc(
                            self.split_unescaped(data_fields[3], ','))))
        if len(data_fields) > 4 and data_fields[4].strip():
            name.set_suffix(' '.join(self.unesc(
                             self.split_unescaped(data_fields[4], ','))))

        self.person.set_primary_name(name)
        return True

    def add_firstname(self, given_name, additional_names, name):
        """
        Combine given_name and additional_names and add as firstname to name.

        If possible try to add given_name as call name.
        """
        default = "%s %s" % (given_name, additional_names)
        if self.formatted_name:
            if given_name:
                if additional_names:
                    given_name_pos = self.formatted_name.find(given_name)
                    if given_name_pos != -1:
                        add_names_pos = self.formatted_name.find(additional_names)
                        if add_names_pos != -1:
                            if given_name_pos <= add_names_pos:
                                firstname = default
                                # Uncertain if given name is used as callname
                            else:
                                firstname = "%s %s" % (additional_names,
                                                       given_name)
                                name.set_call_name(given_name)
                        else:
                            idx = fitin(self.formatted_name, additional_names,
                                        given_name)
                            if idx == -1:
                                # Additional names is not in formatted name
                                firstname = default
                            else:  # Given name in middle of additional names
                                firstname = "%s%s %s" % (additional_names[:idx],
                                             given_name, additional_names[idx:])
                                name.set_call_name(given_name)
                    else:  # Given name is not in formatted name
                        firstname = default
                else:  # There are no additional_names
                    firstname = given_name
            else:  # There is no given_name
                firstname = additional_names
        else:  # There is no formatted name
            firstname = default
        name.set_first_name(firstname.strip())
        return

    def add_nicknames(self, fields, data):
        """Read the NICKNAME property of a VCard."""
        for nick in self.split_unescaped(data, ','):
            nickname = nick.strip()
            if nickname:
                name = Name()
                name.set_nick_name(self.unesc(nickname))
                self.person.add_alternate_name(name)

    def add_sortas(self, fields, data):
        """Read the SORT-STRING property of a VCard."""
        # TODO
        pass

    def add_address(self, fields, data):
        """Read the ADR property of a VCard."""
        data_fields = self.split_unescaped(data, ';')
        data_fields = [x.strip() for x in self.unesc(data_fields)]
        if ''.join(data_fields):
            addr = Address()
            def add_street(strng):
                if strng:
                    already = addr.get_street()
                    if already:
                        addr.set_street("%s %s" % (already, strng))
                    else:
                        addr.set_street(strng)
            addr.add_street = add_street
            set_func = ['add_street', 'add_street', 'add_street', 'set_city',
                        'set_state', 'set_postal_code', 'set_country']
            for i, data in enumerate(data_fields):
                if i >= len(set_func):
                    break
                getattr(addr, set_func[i])(data)
            self.person.add_address(addr)

    def add_phone(self, fields, data):
        """Read the TEL property of a VCard."""
        tel = data.strip()
        if tel:
            addr = Address()
            addr.set_phone(self.unesc(tel))
            self.person.add_address(addr)

    def add_birthday(self, fields, data):
        """Read the BDAY property of a VCard."""
        date_str = data.strip()
        date_match = VCardParser.DATE_RE.match(date_str)
        date = Date()
        if date_match:
            if date_match.group(2):
                date_str = "%s-%s-%s" % (date_match.group(2),
                                       date_match.group(3), date_match.group(4))
            else:
                date_str = date_match.group(1)
            y, m, d = [int(x, 10) for x in date_str.split('-')]
            try:
                date.set(value=(d, m, y, False))
            except DateError:
                # TRANSLATORS: leave the {vcard_snippet} untranslated
                # in the format string, but you may re-order it if needed.
                self.__add_msg(_(
                    "Invalid date in BDAY {vcard_snippet}, "
                    "preserving date as text."
                    ).format(vcard_snippet=data), self.line_num - 1)
                date.set(modifier=Date.MOD_TEXTONLY, text=data)
        else:
            if date_str:
                # TRANSLATORS: leave the {vcard_snippet} untranslated.
                self.__add_msg(_(
                    "Date {vcard_snippet} not in appropriate format "
                    "yyyy-mm-dd, preserving date as text."
                    ).format(vcard_snippet=date_str), self.line_num - 1)
                date.set(modifier=Date.MOD_TEXTONLY, text=date_str)
            else:  # silently ignore an empty BDAY record
                return
        event = Event()
        event.set_type(EventType(EventType.BIRTH))
        event.set_date_object(date)
        self.database.add_event(event, self.trans)

        event_ref = EventRef()
        event_ref.set_reference_handle(event.get_handle())
        self.person.set_birth_ref(event_ref)

    def add_occupation(self, fields, data):
        """Read the ROLE property of a VCard."""
        occupation = data.strip()
        if occupation:
            event = Event()
            event.set_type(EventType(EventType.OCCUPATION))
            event.set_description(self.unesc(occupation))
            self.database.add_event(event, self.trans)

            event_ref = EventRef()
            event_ref.set_reference_handle(event.get_handle())
            self.person.add_event_ref(event_ref)

    def add_url(self, fields, data):
        """Read the URL property of a VCard."""
        href = data.strip()
        if href:
            url = Url()
            url.set_path(self.unesc(href))
            self.person.add_url(url)

    def add_email(self, fields, data):
        """Read the EMAIL property of a VCard."""
        email = data.strip()
        if email:
            url = Url()
            url.set_type(UrlType(UrlType.EMAIL))
            url.set_path(self.unesc(email))
            self.person.add_url(url)

    def add_gender(self, fields, data):
        """Read the GENDER property of a VCard."""
        gender_value = data.strip()
        if gender_value:
            gender_value = gender_value.upper()
            gender_value = gender_value[0]
            if gender_value == 'M':
                gender = Person.MALE
            elif gender_value == 'F':
                gender = Person.FEMALE
            else:
                return
            self.person.set_gender(gender)
示例#9
0
class VCardParser:
    """Class to read data in VCard format from a file."""
    DATE_RE = re.compile(
        r'^(\d{4}-\d{1,2}-\d{1,2})|(?:(\d{4})-?(\d\d)-?(\d\d))')
    GROUP_RE = re.compile(
        r'^(?:[-0-9A-Za-z]+\.)?(.+)$')  # see RFC 2425 sec5.8.2
    ESCAPE_CHAR = '\\'
    TOBE_ESCAPED = ['\\', ',', ';']  # order is important
    LINE_CONTINUATION = [' ', '\t']

    @staticmethod
    def name_value_split(data):
        """Property group.name:value split is on first unquoted colon."""
        colon_idx = data.find(':')
        if colon_idx < 1:
            return ()
        quote_count = data.count('"', 0, colon_idx)
        while quote_count % 2 == 1:
            colon_idx = data.find(':', colon_idx + 1)
            quote_count = data.count('"', 0, colon_idx)
        group_name, value = data[:colon_idx], data[colon_idx + 1:]
        name_parts = VCardParser.GROUP_RE.match(group_name)
        return (name_parts.group(1), value)

    @staticmethod
    def unesc(data):
        """Remove VCard escape sequences."""
        if type(data) == type('string'):
            for char in reversed(VCardParser.TOBE_ESCAPED):
                data = data.replace(VCardParser.ESCAPE_CHAR + char, char)
            return data
        elif type(data) == type([]):
            return list(map(VCardParser.unesc, data))
        else:
            raise TypeError("VCard unescaping is not implemented for "
                            "data type %s." % str(type(data)))

    @staticmethod
    def count_escapes(strng):
        """Count the number of escape characters at the end of a string."""
        count = 0
        for char in reversed(strng):
            if char != VCardParser.ESCAPE_CHAR:
                return count
            count += 1
        return count

    @staticmethod
    def split_unescaped(strng, sep):
        """Split on sep if sep is unescaped."""
        strng_parts = strng.split(sep)
        for i in reversed(range(len(strng_parts[:]))):
            if VCardParser.count_escapes(strng_parts[i]) % 2 == 1:
                # the sep was escaped so undo split
                appendix = strng_parts.pop(i + 1)
                strng_parts[i] += sep + appendix
        return strng_parts

    def __init__(self, dbase):
        self.database = dbase
        self.formatted_name = ''
        self.name_parts = ''
        self.next_line = None
        self.trans = None
        self.version = None
        self.person = None
        self.errors = []
        self.number_of_errors = 0

    def __get_next_line(self, filehandle):
        """
        Read and return the line with the next property of the VCard.

        Also if it spans multiple lines (RFC 2425 sec.5.8.1).
        """
        line = self.next_line
        self.next_line = filehandle.readline()
        self.line_num = self.line_num + 1
        while self.next_line and self.next_line[0] in self.LINE_CONTINUATION:
            line = line.rstrip("\n")
            # TODO perhaps next lines superflous because of rU open parameter?
            if len(line) > 0 and line[-1] == "\r":
                line = line[:-1]
            line += self.next_line[1:]
            self.next_line = filehandle.readline()
            self.line_num = self.line_num + 1
        if line:
            line = line.strip()
        else:
            line = None
        return line

    def __add_msg(self, problem, line=None):
        if problem != "":
            self.number_of_errors += 1
        if line:
            message = _("Line %(line)5d: %(prob)s\n") % {
                "line": line,
                "prob": problem
            }
        else:
            message = problem + "\n"
        self.errors.append(message)

    def parse(self, filehandle, user):
        """
        Prepare the database and parse the input file.

        :param filehandle: open file handle positioned at start of the file
        """
        tym = time.time()
        self.person = None
        self.database.disable_signals()
        with DbTxn(_("vCard import"), self.database, batch=True) as self.trans:
            self._parse_vCard_file(filehandle)
        self.database.enable_signals()
        self.database.request_rebuild()
        tym = time.time() - tym
        # translators: leave all/any {...} untranslated
        msg = ngettext('Import Complete: {number_of} second',
                       'Import Complete: {number_of} seconds',
                       tym).format(number_of=tym)
        LOG.debug(msg)
        if self.number_of_errors == 0:
            message = _("VCARD import report: No errors detected")
        else:
            message = _("VCARD import report: %s errors detected\n") % \
                self.number_of_errors
        if hasattr(user.uistate, 'window'):
            parent_window = user.uistate.window
        else:
            parent_window = None
        user.info(message,
                  "".join(self.errors),
                  parent=parent_window,
                  monospaced=True)

    def _parse_vCard_file(self, filehandle):
        """Read each line of the input file and act accordingly."""
        self.next_line = filehandle.readline()
        self.line_num = 1

        while True:
            line = self.__get_next_line(filehandle)
            if line is None:
                break
            if line == "":
                continue

            if line.find(":") == -1:
                continue
            line_parts = self.name_value_split(line)
            if not line_parts:
                continue

            # No check for escaped ; because only fields[0] is used.
            fields = line_parts[0].split(";")

            property_name = fields[0].upper()
            if property_name == "BEGIN":
                self.next_person()
            elif property_name == "END":
                self.finish_person()
            elif property_name == "VERSION":
                self.check_version(fields, line_parts[1])
            elif property_name == "FN":
                self.add_formatted_name(fields, line_parts[1])
            elif property_name == "N":
                self.add_name_parts(fields, line_parts[1])
            elif property_name == "NICKNAME":
                self.add_nicknames(fields, line_parts[1])
            elif property_name == "SORT-STRING":
                self.add_sortas(fields, line_parts[1])
            elif property_name == "ADR":
                self.add_address(fields, line_parts[1])
            elif property_name == "TEL":
                self.add_phone(fields, line_parts[1])
            elif property_name == "BDAY":
                self.add_birthday(fields, line_parts[1])
            elif property_name == "ROLE":
                self.add_occupation(fields, line_parts[1])
            elif property_name == "URL":
                self.add_url(fields, line_parts[1])
            elif property_name == "EMAIL":
                self.add_email(fields, line_parts[1])
            elif property_name == "X-GENDER" or property_name == "GENDER":
                # VCard 3.0 only has X-GENDER, GENDER is 4.0 syntax,
                # but we want to be robust here.
                self.add_gender(fields, line_parts[1])
            elif property_name == "PRODID":
                # Included cause VCards made by Gramps have this prop.
                pass
            else:
                self.__add_msg(
                    _("Token >%(token)s< unknown. line skipped: %(line)s") % {
                        "token": (fields[0], line),
                        "line": self.line_num - 1
                    })

    def finish_person(self):
        """All info has been collected, write to database."""
        if self.person is not None:
            if self.add_name():
                self.database.add_person(self.person, self.trans)
        self.person = None

    def next_person(self):
        """A VCard for another person is started."""
        if self.person is not None:
            self.finish_person()
            self.__add_msg(
                _("BEGIN property not properly closed by END "
                  "property, Gramps can't cope with nested VCards."),
                self.line_num - 1)
        self.person = Person()
        self.formatted_name = ''
        self.name_parts = ''

    def check_version(self, fields, data):
        """Check the version of the VCard, only version 3.0 is supported."""
        self.version = data
        if self.version != "3.0":
            raise GrampsImportError(
                _("Import of VCards version %s is "
                  "not supported by Gramps.") % self.version)

    def add_formatted_name(self, fields, data):
        """Read the FN property of a VCard."""
        if not self.formatted_name:
            self.formatted_name = self.unesc(str(data)).strip()

    def add_name_parts(self, fields, data):
        """Read the N property of a VCard."""
        if not self.name_parts:
            self.name_parts = data.strip()

    def add_name(self):
        """
        Add the name to the person.

        Returns True on success, False on failure.
        """
        if not self.name_parts.strip():
            self.__add_msg(
                _("VCard is malformed missing the compulsory N "
                  "property, so there is no name; skip it."),
                self.line_num - 1)
            return False
        if not self.formatted_name:
            self.__add_msg(
                _("VCard is malformed missing the compulsory FN "
                  "property, get name from N alone."), self.line_num - 1)
        data_fields = self.split_unescaped(self.name_parts, ';')
        if len(data_fields) != 5:
            self.__add_msg(
                _("VCard is malformed wrong number of name "
                  "components."), self.line_num - 1)

        name = Name()
        name.set_type(NameType(NameType.BIRTH))

        if data_fields[0].strip():
            # assume first surname is primary
            for surname_str in self.split_unescaped(data_fields[0], ','):
                surname = Surname()
                prefix, sname = splitof_nameprefix(self.unesc(surname_str))
                surname.set_surname(sname.strip())
                surname.set_prefix(prefix.strip())
                name.add_surname(surname)
            name.set_primary_surname()

        if len(data_fields) > 1 and data_fields[1].strip():
            given_name = ' '.join(
                self.unesc(self.split_unescaped(data_fields[1], ',')))
        else:
            given_name = ''
        if len(data_fields) > 2 and data_fields[2].strip():
            additional_names = ' '.join(
                self.unesc(self.split_unescaped(data_fields[2], ',')))
        else:
            additional_names = ''
        self.add_firstname(given_name.strip(), additional_names.strip(), name)

        if len(data_fields) > 3 and data_fields[3].strip():
            name.set_title(' '.join(
                self.unesc(self.split_unescaped(data_fields[3], ','))))
        if len(data_fields) > 4 and data_fields[4].strip():
            name.set_suffix(' '.join(
                self.unesc(self.split_unescaped(data_fields[4], ','))))

        self.person.set_primary_name(name)
        return True

    def add_firstname(self, given_name, additional_names, name):
        """
        Combine given_name and additional_names and add as firstname to name.

        If possible try to add given_name as call name.
        """
        default = "%s %s" % (given_name, additional_names)
        if self.formatted_name:
            if given_name:
                if additional_names:
                    given_name_pos = self.formatted_name.find(given_name)
                    if given_name_pos != -1:
                        add_names_pos = self.formatted_name.find(
                            additional_names)
                        if add_names_pos != -1:
                            if given_name_pos <= add_names_pos:
                                firstname = default
                                # Uncertain if given name is used as callname
                            else:
                                firstname = "%s %s" % (additional_names,
                                                       given_name)
                                name.set_call_name(given_name)
                        else:
                            idx = fitin(self.formatted_name, additional_names,
                                        given_name)
                            if idx == -1:
                                # Additional names is not in formatted name
                                firstname = default
                            else:  # Given name in middle of additional names
                                firstname = "%s%s %s" % (
                                    additional_names[:idx], given_name,
                                    additional_names[idx:])
                                name.set_call_name(given_name)
                    else:  # Given name is not in formatted name
                        firstname = default
                else:  # There are no additional_names
                    firstname = given_name
            else:  # There is no given_name
                firstname = additional_names
        else:  # There is no formatted name
            firstname = default
        name.set_first_name(firstname.strip())
        return

    def add_nicknames(self, fields, data):
        """Read the NICKNAME property of a VCard."""
        for nick in self.split_unescaped(data, ','):
            nickname = nick.strip()
            if nickname:
                name = Name()
                name.set_nick_name(self.unesc(nickname))
                self.person.add_alternate_name(name)

    def add_sortas(self, fields, data):
        """Read the SORT-STRING property of a VCard."""
        # TODO
        pass

    def add_address(self, fields, data):
        """Read the ADR property of a VCard."""
        data_fields = self.split_unescaped(data, ';')
        data_fields = [x.strip() for x in self.unesc(data_fields)]
        if ''.join(data_fields):
            addr = Address()

            def add_street(strng):
                if strng:
                    already = addr.get_street()
                    if already:
                        addr.set_street("%s %s" % (already, strng))
                    else:
                        addr.set_street(strng)

            addr.add_street = add_street
            set_func = [
                'add_street', 'add_street', 'add_street', 'set_city',
                'set_state', 'set_postal_code', 'set_country'
            ]
            for i, data in enumerate(data_fields):
                if i >= len(set_func):
                    break
                getattr(addr, set_func[i])(data)
            self.person.add_address(addr)

    def add_phone(self, fields, data):
        """Read the TEL property of a VCard."""
        tel = data.strip()
        if tel:
            addr = Address()
            addr.set_phone(self.unesc(tel))
            self.person.add_address(addr)

    def add_birthday(self, fields, data):
        """Read the BDAY property of a VCard."""
        date_str = data.strip()
        date_match = VCardParser.DATE_RE.match(date_str)
        date = Date()
        if date_match:
            if date_match.group(2):
                date_str = "%s-%s-%s" % (date_match.group(2),
                                         date_match.group(3),
                                         date_match.group(4))
            else:
                date_str = date_match.group(1)
            y, m, d = [int(x, 10) for x in date_str.split('-')]
            try:
                date.set(value=(d, m, y, False))
            except DateError:
                # TRANSLATORS: leave the {vcard_snippet} untranslated
                # in the format string, but you may re-order it if needed.
                self.__add_msg(
                    _("Invalid date in BDAY {vcard_snippet}, "
                      "preserving date as text.").format(vcard_snippet=data),
                    self.line_num - 1)
                date.set(modifier=Date.MOD_TEXTONLY, text=data)
        else:
            if date_str:
                # TRANSLATORS: leave the {vcard_snippet} untranslated.
                self.__add_msg(
                    _("Date {vcard_snippet} not in appropriate format "
                      "yyyy-mm-dd, preserving date as text.").format(
                          vcard_snippet=date_str), self.line_num - 1)
                date.set(modifier=Date.MOD_TEXTONLY, text=date_str)
            else:  # silently ignore an empty BDAY record
                return
        event = Event()
        event.set_type(EventType(EventType.BIRTH))
        event.set_date_object(date)
        self.database.add_event(event, self.trans)

        event_ref = EventRef()
        event_ref.set_reference_handle(event.get_handle())
        self.person.set_birth_ref(event_ref)

    def add_occupation(self, fields, data):
        """Read the ROLE property of a VCard."""
        occupation = data.strip()
        if occupation:
            event = Event()
            event.set_type(EventType(EventType.OCCUPATION))
            event.set_description(self.unesc(occupation))
            self.database.add_event(event, self.trans)

            event_ref = EventRef()
            event_ref.set_reference_handle(event.get_handle())
            self.person.add_event_ref(event_ref)

    def add_url(self, fields, data):
        """Read the URL property of a VCard."""
        href = data.strip()
        if href:
            url = Url()
            url.set_path(self.unesc(href))
            self.person.add_url(url)

    def add_email(self, fields, data):
        """Read the EMAIL property of a VCard."""
        email = data.strip()
        if email:
            url = Url()
            url.set_type(UrlType(UrlType.EMAIL))
            url.set_path(self.unesc(email))
            self.person.add_url(url)

    def add_gender(self, fields, data):
        """Read the GENDER property of a VCard."""
        gender_value = data.strip()
        if gender_value:
            gender_value = gender_value.upper()
            gender_value = gender_value[0]
            if gender_value == 'M':
                gender = Person.MALE
            elif gender_value == 'F':
                gender = Person.FEMALE
            else:
                return
            self.person.set_gender(gender)
示例#10
0
    def run_tool(self):
        self.progress = ProgressMeter(_('Running Date Test'),
                                      '',
                                      parent=self.parent_window)
        self.progress.set_pass(_('Generating dates'), 4)
        dates = []
        # first some valid dates
        calendar = Date.CAL_GREGORIAN
        for quality in (Date.QUAL_NONE, Date.QUAL_ESTIMATED,
                        Date.QUAL_CALCULATED):
            for modifier in (Date.MOD_NONE, Date.MOD_BEFORE, Date.MOD_AFTER,
                             Date.MOD_ABOUT):
                for slash1 in (False, True):
                    for month in range(0, 13):
                        for day in (0, 5, 27):
                            if not month and day:
                                continue
                            d = Date()
                            d.set(quality, modifier, calendar,
                                  (day, month, 1789, slash1), "Text comment")
                            dates.append(d)
            for modifier in (Date.MOD_RANGE, Date.MOD_SPAN):
                for slash1 in (False, True):
                    for slash2 in (False, True):
                        for month in range(0, 13):
                            for day in (0, 5, 27):
                                if not month and day:
                                    continue

                                d = Date()
                                d.set(quality, modifier, calendar,
                                      (day, month, 1789, slash1, day, month,
                                       1876, slash2), "Text comment")
                                dates.append(d)

                                if not month:
                                    continue

                                d = Date()
                                d.set(quality, modifier, calendar,
                                      (day, month, 1789, slash1, day,
                                       13 - month, 1876, slash2),
                                      "Text comment")
                                dates.append(d)

                                if not day:
                                    continue

                                d = Date()
                                d.set(quality, modifier, calendar,
                                      (day, month, 1789, slash1, 32 - day,
                                       month, 1876, slash2), "Text comment")
                                dates.append(d)
                                d = Date()
                                d.set(quality, modifier, calendar,
                                      (day, month, 1789, slash1, 32 - day,
                                       13 - month, 1876, slash2),
                                      "Text comment")
                                dates.append(d)
            modifier = Date.MOD_TEXTONLY
            d = Date()
            d.set(quality, modifier, calendar, Date.EMPTY,
                  "This is a textual date")
            dates.append(d)
            self.progress.step()

        # test invalid dates
        #dateval = (4,7,1789,False,5,8,1876,False)
        #for l in range(1,len(dateval)):
        #    d = Date()
        #    try:
        #        d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #              Date.CAL_GREGORIAN,dateval[:l],"Text comment")
        #        dates.append( d)
        #    except DateError, e:
        #        d.set_as_text("Date identified value correctly as invalid.\n%s" % e)
        #        dates.append( d)
        #    except:
        #        d = Date()
        #        d.set_as_text("Date.set Exception %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
        #        dates.append( d)
        #for l in range(1,len(dateval)):
        #    d = Date()
        #    try:
        #        d.set(Date.QUAL_NONE,Date.MOD_SPAN,Date.CAL_GREGORIAN,dateval[:l],"Text comment")
        #        dates.append( d)
        #    except DateError, e:
        #        d.set_as_text("Date identified value correctly as invalid.\n%s" % e)
        #        dates.append( d)
        #    except:
        #        d = Date()
        #        d.set_as_text("Date.set Exception %s" % ("".join(traceback.format_exception(*sys.exc_info())),))
        #        dates.append( d)
        #self.progress.step()
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #      Date.CAL_GREGORIAN,(44,7,1789,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_NONE,
        #      Date.CAL_GREGORIAN,(4,77,1789,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_SPAN,
        #      Date.CAL_GREGORIAN,
        #      (4,7,1789,False,55,8,1876,False),"Text comment")
        #dates.append( d)
        #d = Date()
        #d.set(Date.QUAL_NONE,Date.MOD_SPAN,
        #      Date.CAL_GREGORIAN,
        #      (4,7,1789,False,5,88,1876,False),"Text comment")
        #dates.append( d)

        with DbTxn(_("Date Test Plugin"), self.db, batch=True) as self.trans:
            self.db.disable_signals()
            self.progress.set_pass(_('Generating dates'), len(dates))

            # create pass and fail tags
            pass_handle = self.create_tag(_('Pass'), '#0000FFFF0000')
            fail_handle = self.create_tag(_('Fail'), '#FFFF00000000')

            # now add them as birth to new persons
            i = 1
            for dateval in dates:
                person = Person()
                surname = Surname()
                surname.set_surname("DateTest")
                name = Name()
                name.add_surname(surname)
                name.set_first_name("Test %d" % i)
                person.set_primary_name(name)
                self.db.add_person(person, self.trans)
                bevent = Event()
                bevent.set_type(EventType.BIRTH)
                bevent.set_date_object(dateval)
                bevent.set_description("Date Test %d (source)" % i)
                bevent_h = self.db.add_event(bevent, self.trans)
                bevent_ref = EventRef()
                bevent_ref.set_reference_handle(bevent_h)
                # for the death event display the date as text and parse it back to a new date
                ndate = None
                try:
                    datestr = _dd.display(dateval)
                    try:
                        ndate = _dp.parse(datestr)
                        if not ndate:
                            ndate = Date()
                            ndate.set_as_text("DateParser None")
                            person.add_tag(fail_handle)
                        else:
                            person.add_tag(pass_handle)
                    except:
                        ndate = Date()
                        ndate.set_as_text("DateParser Exception %s" % ("".join(
                            traceback.format_exception(*sys.exc_info())), ))
                        person.add_tag(fail_handle)
                    else:
                        person.add_tag(pass_handle)
                except:
                    ndate = Date()
                    ndate.set_as_text("DateDisplay Exception: %s" % ("".join(
                        traceback.format_exception(*sys.exc_info())), ))
                    person.add_tag(fail_handle)

                if dateval.get_modifier() != Date.MOD_TEXTONLY \
                       and ndate.get_modifier() == Date.MOD_TEXTONLY:
                    # parser was unable to correctly parse the string
                    ndate.set_as_text("TEXTONLY: " + ndate.get_text())
                    person.add_tag(fail_handle)
                if dateval.get_modifier() == Date.MOD_TEXTONLY \
                        and dateval.get_text().count("Traceback") \
                        and pass_handle in person.get_tag_list():
                    person.add_tag(fail_handle)

                devent = Event()
                devent.set_type(EventType.DEATH)
                devent.set_date_object(ndate)
                devent.set_description("Date Test %d (result)" % i)
                devent_h = self.db.add_event(devent, self.trans)
                devent_ref = EventRef()
                devent_ref.set_reference_handle(devent_h)
                person.set_birth_ref(bevent_ref)
                person.set_death_ref(devent_ref)
                self.db.commit_person(person, self.trans)
                i = i + 1
                self.progress.step()
        self.db.enable_signals()
        self.db.request_rebuild()
        self.progress.close()