1# encoding:utf-8
2#
3# Gramps - a GTK+/GNOME based genealogy program - Records plugin
4#
5# Copyright (C) 2008-2011 Reinhard Müller
6# Copyright (C) 2010      Jakim Friant
7# Copyright (C) 2012      Brian G. Matherly
8# Copyright (C) 2013-2016 Paul Franklin
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23#
24
25""" Records Report """
26
27#------------------------------------------------------------------------
28#
29# Standard Python modules
30#
31#------------------------------------------------------------------------
32
33#------------------------------------------------------------------------
34#
35# Gramps modules
36#
37#------------------------------------------------------------------------
38from gramps.gen.const import GRAMPS_LOCALE as glocale
39_ = glocale.translation.sgettext
40from gramps.plugins.lib.librecords import (RECORDS, find_records,
41                                           CALLNAME_DONTUSE, CALLNAME_REPLACE,
42                                           CALLNAME_UNDERLINE_ADD)
43from gramps.gen.plug.docgen import (FontStyle, ParagraphStyle,
44                                    FONT_SANS_SERIF, PARA_ALIGN_CENTER,
45                                    IndexMark, INDEX_TYPE_TOC)
46from gramps.gen.plug.menu import (BooleanOption, EnumeratedListOption,
47                                  FilterOption, NumberOption,
48                                  PersonOption, StringOption)
49from gramps.gen.plug.report import Report
50from gramps.gen.plug.report import utils
51from gramps.gen.plug.report import MenuReportOptions
52from gramps.gen.plug.report import stdoptions
53from gramps.gen.lib import Span
54from gramps.gen.errors import ReportError
55from gramps.gen.proxy import LivingProxyDb, CacheProxyDb
56
57#------------------------------------------------------------------------
58#
59# Records Report
60#
61#------------------------------------------------------------------------
62class RecordsReport(Report):
63    """ Records Report """
64
65    def __init__(self, database, options, user):
66        """
67        This report needs the following parameters (class variables)
68        that come in the options class.
69
70        incl_private    - Whether to include private data
71        living_people - How to handle living people
72        years_past_death - Consider as living this many years after death
73        """
74
75        Report.__init__(self, database, options, user)
76        menu = options.menu
77
78        self.set_locale(options.menu.get_option_by_name('trans').get_value())
79
80        stdoptions.run_private_data_option(self, menu)
81        living_opt = stdoptions.run_living_people_option(self, menu,
82                                                         self._locale)
83        self.database = CacheProxyDb(self.database)
84
85        self._lv = menu.get_option_by_name('living_people').get_value()
86        for (value, description) in living_opt.get_items(xml_items=True):
87            if value == self._lv:
88                living_desc = self._(description)
89                break
90        self.living_desc = self._(
91            "(Living people: %(option_name)s)") % {'option_name': living_desc}
92
93        filter_option = menu.get_option_by_name('filter')
94        self.filter = filter_option.get_filter()
95
96        self.top_size = menu.get_option_by_name('top_size').get_value()
97        self.callname = menu.get_option_by_name('callname').get_value()
98
99        self.footer = menu.get_option_by_name('footer').get_value()
100
101        self.include = {}
102        for (text, varname, default) in RECORDS:
103            self.include[varname] = menu.get_option_by_name(varname).get_value()
104
105        self._nf = stdoptions.run_name_format_option(self, menu)
106
107    def write_report(self):
108        """
109        Build the actual report.
110        """
111
112        records = find_records(self.database, self.filter,
113                               self.top_size, self.callname,
114                               trans_text=self._, name_format=self._nf,
115                               living_mode=self._lv, user=self._user)
116
117        self.doc.start_paragraph('REC-Title')
118        title = self._("Records")
119        mark = IndexMark(title, INDEX_TYPE_TOC, 1)
120        self.doc.write_text(title, mark)
121        self.doc.end_paragraph()
122
123        self.doc.start_paragraph('REC-Subtitle')
124        filter_name = self.filter.get_name(self._locale)
125        self.doc.write_text("(%s)" % filter_name)
126        self.doc.end_paragraph()
127        if self._lv != LivingProxyDb.MODE_INCLUDE_ALL:
128            self.doc.start_paragraph('REC-Subtitle')
129            self.doc.write_text(self.living_desc)
130            self.doc.end_paragraph()
131
132        for (text, varname, top) in records:
133            if not self.include[varname]:
134                continue
135
136            self.doc.start_paragraph('REC-Heading')
137            self.doc.write_text(self._(text))
138            self.doc.end_paragraph()
139
140            last_value = None
141            rank = 0
142            for (number,
143                 (sort, value, name, handletype, handle)) in enumerate(top):
144                mark = None
145                if handletype == 'Person':
146                    person = self.database.get_person_from_handle(handle)
147                    mark = utils.get_person_mark(self.database, person)
148                elif handletype == 'Family':
149                    family = self.database.get_family_from_handle(handle)
150                    # librecords.py checks that the family has both
151                    # a father and a mother and also that each one is
152                    # in the filter if any filter was used, so we don't
153                    # have to do any similar checking here, it's been done
154                    f_handle = family.get_father_handle()
155                    dad = self.database.get_person_from_handle(f_handle)
156                    f_mark = utils.get_person_mark(self.database, dad)
157                    m_handle = family.get_mother_handle()
158                    mom = self.database.get_person_from_handle(m_handle)
159                    m_mark = utils.get_person_mark(self.database, mom)
160                else:
161                    raise ReportError(_(
162                        "Option '%(opt_name)s' is present "
163                        "in %(file)s\n  but is not known to "
164                        "the module.  Ignoring...")
165                                      % {'opt_name': handletype,
166                                         'file': 'libnarrate.py'})
167                    # since the error is very unlikely I reused the string
168                if value != last_value:
169                    last_value = value
170                    rank = number
171                self.doc.start_paragraph('REC-Normal')
172                self.doc.write_text(
173                    self._("%(number)s. ") % {'number': rank+1})
174                self.doc.write_markup(str(name), name.get_tags(), mark)
175                if handletype == 'Family':
176                    self.doc.write_text('', f_mark)
177                    self.doc.write_text('', m_mark)
178                if isinstance(value, Span):
179                    tvalue = value.get_repr(dlocale=self._locale)
180                else:
181                    tvalue = value
182                self.doc.write_text(" (%s)" % tvalue)
183                self.doc.end_paragraph()
184
185        self.doc.start_paragraph('REC-Footer')
186        self.doc.write_text(self.footer)
187        self.doc.end_paragraph()
188
189
190#------------------------------------------------------------------------
191#
192# Records Report Options
193#
194#------------------------------------------------------------------------
195class RecordsReportOptions(MenuReportOptions):
196    """
197    Defines options and provides handling interface.
198    """
199
200    def __init__(self, name, dbase):
201
202        self.__pid = None
203        self.__filter = None
204        self.__db = dbase
205        self._nf = None
206        MenuReportOptions.__init__(self, name, dbase)
207
208
209    def get_subject(self):
210        """ Return a string that describes the subject of the report. """
211        return self.__filter.get_filter().get_name()
212
213    def add_menu_options(self, menu):
214
215        category_name = _("Report Options")
216
217        self.__filter = FilterOption(_("Filter"), 0)
218        self.__filter.set_help(
219            _("Determines what people are included in the report."))
220        menu.add_option(category_name, "filter", self.__filter)
221        self.__filter.connect('value-changed', self.__filter_changed)
222
223        self.__pid = PersonOption(_("Filter Person"))
224        self.__pid.set_help(_("The center person for the filter"))
225        menu.add_option(category_name, "pid", self.__pid)
226        self.__pid.connect('value-changed', self.__update_filters)
227
228        top_size = NumberOption(_("Number of ranks to display"), 3, 1, 100)
229        menu.add_option(category_name, "top_size", top_size)
230
231        callname = EnumeratedListOption(_("Use call name"), CALLNAME_DONTUSE)
232        callname.set_items([
233            (CALLNAME_DONTUSE, _("Don't use call name")),
234            (CALLNAME_REPLACE, _("Replace first names with call name")),
235            (CALLNAME_UNDERLINE_ADD,
236             _("Underline call name in first names / "
237               "add call name to first name"))])
238        menu.add_option(category_name, "callname", callname)
239
240        footer = StringOption(_("Footer text"), "")
241        menu.add_option(category_name, "footer", footer)
242
243        category_name = _("Report Options (2)")
244
245        self._nf = stdoptions.add_name_format_option(menu, category_name)
246        self._nf.connect('value-changed', self.__update_filters)
247
248        self.__update_filters()
249
250        stdoptions.add_private_data_option(menu, category_name)
251
252        stdoptions.add_living_people_option(menu, category_name)
253
254        stdoptions.add_localization_option(menu, category_name)
255
256        p_count = 0
257        for (text, varname, default) in RECORDS:
258            if varname.startswith('person'):
259                p_count += 1
260        p_half = p_count // 2
261        p_idx = 0
262        for (text, varname, default) in RECORDS:
263            option = BooleanOption(_(text), default)
264            if varname.startswith('person'):
265                if p_idx >= p_half:
266                    category_name = _("Person 2")
267                else:
268                    category_name = _("Person 1")
269                p_idx += 1
270            elif varname.startswith('family'):
271                category_name = _("Family")
272            menu.add_option(category_name, varname, option)
273
274    def __update_filters(self):
275        """
276        Update the filter list based on the selected person
277        """
278        gid = self.__pid.get_value()
279        person = self.__db.get_person_from_gramps_id(gid)
280        nfv = self._nf.get_value()
281        filter_list = utils.get_person_filters(person,
282                                               include_single=False,
283                                               name_format=nfv)
284        self.__filter.set_filters(filter_list)
285
286    def __filter_changed(self):
287        """
288        Handle filter change. If the filter is not specific to a person,
289        disable the person option
290        """
291        filter_value = self.__filter.get_value()
292        if filter_value == 0: # "Entire Database" (as "include_single=False")
293            self.__pid.set_available(False)
294        else:
295            # The other filters need a center person (assume custom ones too)
296            self.__pid.set_available(True)
297
298    def make_default_style(self, default_style):
299
300        #Paragraph Styles
301        font = FontStyle()
302        font.set_type_face(FONT_SANS_SERIF)
303        font.set_size(16)
304        font.set_bold(True)
305        para = ParagraphStyle()
306        para.set_font(font)
307        para.set_alignment(PARA_ALIGN_CENTER)
308        para.set_description(_("The style used for the title."))
309        default_style.add_paragraph_style('REC-Title', para)
310
311        font = FontStyle()
312        font.set_type_face(FONT_SANS_SERIF)
313        font.set_size(12)
314        font.set_bold(True)
315        para = ParagraphStyle()
316        para.set_font(font)
317        para.set_alignment(PARA_ALIGN_CENTER)
318        para.set_description(_("The style used for the subtitle."))
319        default_style.add_paragraph_style('REC-Subtitle', para)
320
321        font = FontStyle()
322        font.set_size(12)
323        font.set_bold(True)
324        para = ParagraphStyle()
325        para.set_font(font)
326        para.set_top_margin(utils.pt2cm(6))
327        para.set_description(_('The style used for the section headers.'))
328        default_style.add_paragraph_style('REC-Heading', para)
329
330        font = FontStyle()
331        font.set_size(10)
332        para = ParagraphStyle()
333        para.set_font(font)
334        para.set_left_margin(0.5)
335        para.set_description(_('The basic style used for the text display.'))
336        default_style.add_paragraph_style('REC-Normal', para)
337
338        font = FontStyle()
339        font.set_size(8)
340        para = ParagraphStyle()
341        para.set_font(font)
342        para.set_alignment(PARA_ALIGN_CENTER)
343        para.set_top_border(True)
344        para.set_top_margin(utils.pt2cm(8))
345        para.set_description(_('The style used for the footer.'))
346        default_style.add_paragraph_style('REC-Footer', para)
347