1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2008,2011  Gary Burton
5# Copyright (C) 2010       Jakim Friant
6# Copyright (C) 2011       Heinz Brinker
7# Copyright (C) 2013-2016  Paul Franklin
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22#
23
24"""Place Report"""
25
26#------------------------------------------------------------------------
27#
28# python modules
29#
30#------------------------------------------------------------------------
31
32#------------------------------------------------------------------------
33#
34# gramps modules
35#
36#------------------------------------------------------------------------
37from gramps.gen.const import GRAMPS_LOCALE as glocale
38_ = glocale.translation.sgettext
39from gramps.gen.plug.menu import (FilterOption, PlaceListOption,
40                                  EnumeratedListOption)
41from gramps.gen.plug.report import Report
42from gramps.gen.plug.report import MenuReportOptions
43from gramps.gen.plug.report import stdoptions
44from gramps.gen.plug.docgen import (IndexMark, FontStyle, ParagraphStyle,
45                                    TableStyle, TableCellStyle,
46                                    FONT_SANS_SERIF, FONT_SERIF,
47                                    INDEX_TYPE_TOC, PARA_ALIGN_CENTER)
48from gramps.gen.sort import Sort
49from gramps.gen.utils.location import get_location_list
50from gramps.gen.display.place import displayer as _pd
51from gramps.gen.errors import ReportError
52from gramps.gen.proxy import LivingProxyDb, CacheProxyDb
53
54class PlaceReport(Report):
55    """
56    Place Report class
57    """
58    def __init__(self, database, options, user):
59        """
60        Create the PlaceReport object produces the Place report.
61
62        The arguments are:
63
64        database        - the Gramps database instance
65        options         - instance of the Options class for this report
66        user            - instance of a gen.user.User class
67
68        This report needs the following parameters (class variables)
69        that come in the options class.
70
71        places          - List of places to report on.
72        center          - Center of report, person or event
73        incl_private    - Whether to include private data
74        name_format     - Preferred format to display names
75        living_people - How to handle living people
76        years_past_death - Consider as living this many years after death
77        """
78
79        Report.__init__(self, database, options, user)
80
81        self._user = user
82        menu = options.menu
83
84        self.set_locale(menu.get_option_by_name('trans').get_value())
85
86        stdoptions.run_date_format_option(self, menu)
87
88        stdoptions.run_private_data_option(self, menu)
89        living_opt = stdoptions.run_living_people_option(self, menu,
90                                                         self._locale)
91        self.database = CacheProxyDb(self.database)
92        self._db = self.database
93
94        self._lv = menu.get_option_by_name('living_people').get_value()
95        for (value, description) in living_opt.get_items(xml_items=True):
96            if value == self._lv:
97                living_desc = self._(description)
98                break
99        self.living_desc = self._("(Living people: %(option_name)s)"
100                                 ) % {'option_name': living_desc}
101
102        places = menu.get_option_by_name('places').get_value()
103        self.center = menu.get_option_by_name('center').get_value()
104
105        stdoptions.run_name_format_option(self, menu)
106        self._nd = self._name_display
107
108        self.place_format = menu.get_option_by_name("place_format").get_value()
109
110        filter_option = menu.get_option_by_name('filter')
111        self.filter = filter_option.get_filter()
112
113        self.sort = Sort(self._db)
114
115        self.place_handles = []
116        if self.filter.get_name() != '':
117            # Use the selected filter to provide a list of place handles
118            plist = self._db.iter_place_handles()
119            self.place_handles = self.filter.apply(self._db, plist,
120                                                   user=self._user)
121
122        if places:
123            # Add places selected individually
124            self.place_handles += self.__get_place_handles(places)
125
126        if not self.place_handles:
127            raise ReportError(
128                _('Place Report'),
129                _('Please select at least one place before running this.'))
130
131        self.place_handles.sort(key=self.sort.by_place_title_key)
132
133    def write_report(self):
134        """
135        The routine that actually creates the report.
136        At this point, the document is opened and ready for writing.
137        """
138
139        # Write the title line. Set in INDEX marker so that this section will be
140        # identified as a major category if this is included in a Book report.
141
142        title = self._("Place Report")
143        mark = IndexMark(title, INDEX_TYPE_TOC, 1)
144        self.doc.start_paragraph("PLC-ReportTitle")
145        self.doc.write_text(title, mark)
146        self.doc.end_paragraph()
147        if self._lv != LivingProxyDb.MODE_INCLUDE_ALL:
148            self.doc.start_paragraph("PLC-ReportSubtitle")
149            self.doc.write_text(self.living_desc)
150            self.doc.end_paragraph()
151        self.__write_all_places()
152
153    def __write_all_places(self):
154        """
155        This procedure writes out each of the selected places.
156        """
157        place_nbr = 1
158
159        with self._user.progress(_("Place Report"),
160                                 _("Generating report"),
161                                 len(self.place_handles)) as step:
162
163            for handle in self.place_handles:
164                self.__write_place(handle, place_nbr)
165                if self.center == "Event":
166                    self.__write_referenced_events(handle)
167                elif self.center == "Person":
168                    self.__write_referenced_persons(handle)
169                else:
170                    raise AttributeError("no such center: '%s'" % self.center)
171                place_nbr += 1
172                # increment progress bar
173                step()
174
175
176    def __write_place(self, handle, place_nbr):
177        """
178        This procedure writes out the details of a single place
179        """
180        place = self._db.get_place_from_handle(handle)
181
182        place_details = [self._("Gramps ID: %s ") % place.get_gramps_id()]
183        for level in get_location_list(self._db, place):
184            # translators: needed for French, ignore otherwise
185            place_details.append(self._("%(str1)s: %(str2)s"
186                                       ) % {'str1': self._(level[1].xml_str()),
187                                            'str2': level[0]})
188
189        place_names = ''
190        all_names = place.get_all_names()
191        if len(all_names) > 1 or __debug__:
192            for place_name in all_names:
193                if place_names != '':
194                    # translators: needed for Arabic, ignore otherwise
195                    place_names += self._(", ")
196                place_names += '%s' % place_name.get_value()
197                if place_name.get_language() != '' or __debug__:
198                    place_names += ' (%s)' % place_name.get_language()
199            place_details += [self._("places|All Names: %s") % place_names,]
200        self.doc.start_paragraph("PLC-PlaceTitle")
201        place_title = _pd.display(self._db, place, None, self.place_format)
202        self.doc.write_text(("%(nbr)s. %(place)s") % {'nbr' : place_nbr,
203                                                      'place' : place_title})
204        self.doc.end_paragraph()
205
206        for item in place_details:
207            self.doc.start_paragraph("PLC-PlaceDetails")
208            self.doc.write_text(item)
209            self.doc.end_paragraph()
210
211    def __write_referenced_events(self, handle):
212        """
213        This procedure writes out each of the events related to the place
214        """
215        event_handles = [event_handle for (object_type, event_handle) in
216                         self._db.find_backlink_handles(handle, ['Event'])]
217        event_handles.sort(key=self.sort.by_date_key)
218
219        if event_handles:
220            self.doc.start_paragraph("PLC-Section")
221            title = self._("Events that happened at this place")
222            self.doc.write_text(title)
223            self.doc.end_paragraph()
224            self.doc.start_table("EventTable", "PLC-EventTable")
225            column_titles = [self._("Date"), self._("Type of Event"),
226                             self._("Person"), self._("Description")]
227            self.doc.start_row()
228            for title in column_titles:
229                self.doc.start_cell("PLC-TableColumn")
230                self.doc.start_paragraph("PLC-ColumnTitle")
231                self.doc.write_text(title)
232                self.doc.end_paragraph()
233                self.doc.end_cell()
234            self.doc.end_row()
235
236        for evt_handle in event_handles:
237            event = self._db.get_event_from_handle(evt_handle)
238            if event: # will be None if marked private
239                date = self._get_date(event.get_date_object())
240                descr = event.get_description()
241                event_type = self._(self._get_type(event.get_type()))
242
243                person_list = []
244                ref_handles = [x for x in
245                               self._db.find_backlink_handles(evt_handle)]
246                if not ref_handles: # since the backlink may point to private
247                    continue        # data, ignore an event with no backlinks
248                for (ref_type, ref_handle) in ref_handles:
249                    if ref_type == 'Person':
250                        person_list.append(ref_handle)
251                    else:
252                        family = self._db.get_family_from_handle(ref_handle)
253                        father = family.get_father_handle()
254                        if father:
255                            person_list.append(father)
256                        mother = family.get_mother_handle()
257                        if mother:
258                            person_list.append(mother)
259
260                people = ""
261                person_list = list(set(person_list))
262                for p_handle in person_list:
263                    person = self._db.get_person_from_handle(p_handle)
264                    if person:
265                        person_name = self._nd.display(person)
266                        if people == "":
267                            people = "%(name)s (%(id)s)" % {
268                                'name' : person_name,
269                                'id'   : person.get_gramps_id()}
270                        else:
271                            people = self._("%(persons)s and %(name)s (%(id)s)"
272                                           ) % {'persons' : people,
273                                                'name'    : person_name,
274                                                'id' : person.get_gramps_id()}
275
276                event_details = [date, event_type, people, descr]
277                self.doc.start_row()
278                for detail in event_details:
279                    self.doc.start_cell("PLC-Cell")
280                    self.doc.start_paragraph("PLC-Details")
281                    self.doc.write_text("%s " % detail)
282                    self.doc.end_paragraph()
283                    self.doc.end_cell()
284                self.doc.end_row()
285
286        if event_handles:
287            self.doc.end_table()
288
289    def __write_referenced_persons(self, handle):
290        """
291        This procedure writes out each of the people related to the place
292        """
293        event_handles = [event_handle for (object_type, event_handle) in
294                         self._db.find_backlink_handles(handle, ['Event'])]
295
296        if event_handles:
297            self.doc.start_paragraph("PLC-Section")
298            title = self._("People associated with this place")
299            self.doc.write_text(title)
300            self.doc.end_paragraph()
301            self.doc.start_table("EventTable", "PLC-PersonTable")
302            column_titles = [self._("Person"), self._("Type of Event"), \
303                             self._("Description"), self._("Date")]
304            self.doc.start_row()
305            for title in column_titles:
306                self.doc.start_cell("PLC-TableColumn")
307                self.doc.start_paragraph("PLC-ColumnTitle")
308                self.doc.write_text(title)
309                self.doc.end_paragraph()
310                self.doc.end_cell()
311            self.doc.end_row()
312
313        person_dict = {}
314        for evt_handle in event_handles:
315            ref_handles = [x for x in
316                           self._db.find_backlink_handles(evt_handle)]
317            for (ref_type, ref_handle) in ref_handles:
318                if ref_type == 'Person':
319                    person = self._db.get_person_from_handle(ref_handle)
320                    name_entry = "%s (%s)" % (self._nd.display(person),
321                                              person.get_gramps_id())
322                else:
323                    family = self._db.get_family_from_handle(ref_handle)
324                    f_handle = family.get_father_handle()
325                    m_handle = family.get_mother_handle()
326                    if f_handle and m_handle:
327                        father = self._db.get_person_from_handle(f_handle)
328                        mother = self._db.get_person_from_handle(m_handle)
329                        father_name = self._nd.display(father)
330                        mother_name = self._nd.display(mother)
331                        father_id = father.get_gramps_id()
332                        mother_id = mother.get_gramps_id()
333                        name_entry = self._("%(father)s (%(father_id)s) and "
334                                            "%(mother)s (%(mother_id)s)"
335                                           ) % {'father'    : father_name,
336                                                'father_id' : father_id,
337                                                'mother'    : mother_name,
338                                                'mother_id' : mother_id}
339                    elif f_handle or m_handle:
340                        if f_handle:
341                            p_handle = f_handle
342                        else:
343                            p_handle = m_handle
344                        person = self._db.get_person_from_handle(p_handle)
345
346                        name_entry = "%s (%s)" % (self._nd.display(person),
347                                                  person.get_gramps_id())
348                    else:
349                        # No parents - bug #7299
350                        continue
351
352                if name_entry in person_dict:
353                    person_dict[name_entry].append(evt_handle)
354                else:
355                    person_dict[name_entry] = []
356                    person_dict[name_entry].append(evt_handle)
357
358        keys = list(person_dict.keys())
359        keys.sort()
360
361        for entry in keys:
362            people = entry
363            person_dict[entry].sort(key=self.sort.by_date_key)
364            for evt_handle in person_dict[entry]:
365                event = self._db.get_event_from_handle(evt_handle)
366                if event:
367                    date = self._get_date(event.get_date_object())
368                    descr = event.get_description()
369                    event_type = self._(self._get_type(event.get_type()))
370                else:
371                    date = ''
372                    descr = ''
373                    event_type = ''
374                event_details = [people, event_type, descr, date]
375                self.doc.start_row()
376                for detail in event_details:
377                    self.doc.start_cell("PLC-Cell")
378                    self.doc.start_paragraph("PLC-Details")
379                    self.doc.write_text("%s " % detail)
380                    self.doc.end_paragraph()
381                    self.doc.end_cell()
382                people = "" # do not repeat the name on the next event
383                self.doc.end_row()
384
385        if event_handles:
386            self.doc.end_table()
387
388    def __get_place_handles(self, places):
389        """
390        This procedure converts a string of place GIDs to a list of handles
391        """
392        place_handles = []
393        for place_gid in places.split():
394            place = self._db.get_place_from_gramps_id(place_gid)
395            if place is not None:
396                #place can be None if option is gid of other fam tree
397                place_handles.append(place.get_handle())
398
399        return place_handles
400
401#------------------------------------------------------------------------
402#
403# PlaceOptions
404#
405#------------------------------------------------------------------------
406class PlaceOptions(MenuReportOptions):
407
408    """
409    Defines options and provides handling interface.
410    """
411
412    def __init__(self, name, dbase):
413        self.__db = dbase
414        self.__filter = None
415        self.__places = None
416        self.__pf = None
417        MenuReportOptions.__init__(self, name, dbase)
418
419    def get_subject(self):
420        """ Return a string that describes the subject of the report. """
421        subject = ""
422        if self.__filter.get_filter().get_name():
423            # Use the selected filter's name, if any
424            subject += self.__filter.get_filter().get_name()
425        if self.__places.get_value():
426            # Add places selected individually, if any
427            for place_id in self.__places.get_value().split():
428                if subject:
429                    subject += " + "
430                place = self.__db.get_place_from_gramps_id(place_id)
431                subject += _pd.display(self.__db, place, None,
432                                       self.__pf.get_value())
433        return subject
434
435    def add_menu_options(self, menu):
436        """
437        Add options to the menu for the place report.
438        """
439        category_name = _("Report Options")
440
441        # Reload filters to pick any new ones
442        CustomFilters = None
443        from gramps.gen.filters import CustomFilters, GenericFilter
444
445        self.__filter = FilterOption(_("Select using filter"), 0)
446        self.__filter.set_help(_("Select places using a filter"))
447        filter_list = []
448        filter_list.append(GenericFilter())
449        filter_list.extend(CustomFilters.get_filters('Place'))
450        self.__filter.set_filters(filter_list)
451        menu.add_option(category_name, "filter", self.__filter)
452
453        self.__places = PlaceListOption(_("Select places individually"))
454        self.__places.set_help(_("List of places to report on"))
455        menu.add_option(category_name, "places", self.__places)
456
457        center = EnumeratedListOption(_("Center on"), "Event")
458        center.set_items([("Event", _("Event")), ("Person", _("Person"))])
459        center.set_help(_("If report is event or person centered"))
460        menu.add_option(category_name, "center", center)
461
462        category_name = _("Report Options (2)")
463
464        stdoptions.add_name_format_option(menu, category_name)
465
466        self.__pf = stdoptions.add_place_format_option(menu, category_name)
467
468        stdoptions.add_private_data_option(menu, category_name)
469
470        stdoptions.add_living_people_option(menu, category_name)
471
472        locale_opt = stdoptions.add_localization_option(menu, category_name)
473
474        stdoptions.add_date_format_option(menu, category_name, locale_opt)
475
476    def make_default_style(self, default_style):
477        """
478        Make the default output style for the Place report.
479        """
480        self.default_style = default_style
481        self.__report_title_style()
482        self.__report_subtitle_style()
483        self.__place_title_style()
484        self.__place_details_style()
485        self.__column_title_style()
486        self.__section_style()
487        self.__event_table_style()
488        self.__details_style()
489        self.__cell_style()
490        self.__table_column_style()
491
492    def __report_title_style(self):
493        """
494        Define the style used for the report title
495        """
496        font = FontStyle()
497        font.set(face=FONT_SANS_SERIF, size=16, bold=1)
498        para = ParagraphStyle()
499        para.set_font(font)
500        para.set_header_level(1)
501        para.set_top_margin(0.25)
502        para.set_bottom_margin(0.25)
503        para.set_alignment(PARA_ALIGN_CENTER)
504        para.set_description(_('The style used for the title.'))
505        self.default_style.add_paragraph_style("PLC-ReportTitle", para)
506
507    def __report_subtitle_style(self):
508        """
509        Define the style used for the report subtitle
510        """
511        font = FontStyle()
512        font.set(face=FONT_SANS_SERIF, size=12, bold=1)
513        para = ParagraphStyle()
514        para.set_font(font)
515        para.set_header_level(1)
516        para.set_top_margin(0.25)
517        para.set_bottom_margin(0.25)
518        para.set_alignment(PARA_ALIGN_CENTER)
519        para.set_description(_('The style used for the subtitle.'))
520        self.default_style.add_paragraph_style("PLC-ReportSubtitle", para)
521
522    def __place_title_style(self):
523        """
524        Define the style used for the place title
525        """
526        font = FontStyle()
527        font.set(face=FONT_SERIF, size=12, italic=0, bold=1)
528        para = ParagraphStyle()
529        para.set_font(font)
530        para.set(first_indent=-1.5, lmargin=1.5)
531        para.set_top_margin(0.75)
532        para.set_bottom_margin(0.25)
533        para.set_description(_('The style used for the section headers.'))
534        self.default_style.add_paragraph_style("PLC-PlaceTitle", para)
535
536    def __place_details_style(self):
537        """
538        Define the style used for the place details
539        """
540        font = FontStyle()
541        font.set(face=FONT_SERIF, size=10)
542        para = ParagraphStyle()
543        para.set_font(font)
544        para.set(first_indent=0.0, lmargin=1.5)
545        para.set_description(_('The style used for details.'))
546        self.default_style.add_paragraph_style("PLC-PlaceDetails", para)
547
548    def __column_title_style(self):
549        """
550        Define the style used for the event table column title
551        """
552        font = FontStyle()
553        font.set(face=FONT_SERIF, size=10, bold=1)
554        para = ParagraphStyle()
555        para.set_font(font)
556        para.set(first_indent=0.0, lmargin=0.0)
557        para.set_description(_('The basic style used for table headings.'))
558        self.default_style.add_paragraph_style("PLC-ColumnTitle", para)
559
560    def __section_style(self):
561        """
562        Define the style used for each section
563        """
564        font = FontStyle()
565        font.set(face=FONT_SERIF, size=10, italic=0, bold=0)
566        para = ParagraphStyle()
567        para.set_font(font)
568        para.set(first_indent=-1.5, lmargin=1.5)
569        para.set_top_margin(0.5)
570        para.set_bottom_margin(0.25)
571        para.set_description(_('The basic style used for the text display.'))
572        self.default_style.add_paragraph_style("PLC-Section", para)
573
574    def __event_table_style(self):
575        """
576        Define the style used for event table
577        """
578        table = TableStyle()
579        table.set_width(100)
580        table.set_columns(4)
581        table.set_column_width(0, 25)
582        table.set_column_width(1, 15)
583        table.set_column_width(2, 35)
584        table.set_column_width(3, 25)
585        self.default_style.add_table_style("PLC-EventTable", table)
586        table.set_width(100)
587        table.set_columns(4)
588        table.set_column_width(0, 35)
589        table.set_column_width(1, 15)
590        table.set_column_width(2, 25)
591        table.set_column_width(3, 25)
592        self.default_style.add_table_style("PLC-PersonTable", table)
593
594    def __details_style(self):
595        """
596        Define the style used for person and event details
597        """
598        font = FontStyle()
599        font.set(face=FONT_SERIF, size=10)
600        para = ParagraphStyle()
601        para.set_font(font)
602        para.set_description(_('The style used for the items and values.'))
603        self.default_style.add_paragraph_style("PLC-Details", para)
604
605    def __cell_style(self):
606        """
607        Define the style used for cells in the event table
608        """
609        cell = TableCellStyle()
610        self.default_style.add_cell_style("PLC-Cell", cell)
611
612    def __table_column_style(self):
613        """
614        Define the style used for event table columns
615        """
616        cell = TableCellStyle()
617        cell.set_bottom_border(1)
618        self.default_style.add_cell_style('PLC-TableColumn', cell)
619