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