Source code for gazette.pdf

from copy import deepcopy
from io import BytesIO
from onegov.gazette import _
from onegov.gazette.layout import Layout
from onegov.gazette.models import Category
from onegov.gazette.models import GazetteNotice
from onegov.gazette.models import IssueName
from onegov.gazette.models import Organization
from onegov.gazette.utils import bool_is
from onegov.pdf import page_fn_footer
from onegov.pdf import page_fn_header_and_footer
from onegov.pdf import page_fn_header_logo_and_footer
from onegov.pdf import Pdf as PdfBase
from pdfdocument.document import MarkupParagraph
from reportlab.platypus.flowables import PageBreak
from sqlalchemy import func


from typing import Any
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from onegov.gazette.collections import GazetteNoticeCollection
    from onegov.gazette.models import Issue
    from onegov.gazette.request import GazetteRequest
    from sqlalchemy.orm import Session
    from uuid import UUID


[docs] class Pdf(PdfBase):
[docs] def adjust_style(self, font_size: int = 10) -> None: """ Adds styles for notices. """ super().adjust_style(font_size) self.style.title = deepcopy(self.style.normal) self.style.title.fontSize = 2.25 * self.style.fontSize self.style.title.leading = 1.2 * self.style.title.fontSize self.style.title.spaceBefore = 0 self.style.title.spaceAfter = 0.67 * self.style.title.fontSize self.style.h_notice = deepcopy(self.style.bold) self.style.h_notice.fontSize = 1.125 * self.style.fontSize self.style.table_h_notice = ( *self.style.table, ('TOPPADDING', (0, 0), (0, 0), 2) ) self.style.paragraph.spaceAfter = 0.675 * self.style.paragraph.fontSize self.style.paragraph.leading = 1.275 * self.style.paragraph.fontSize self.style.ul.bullet = '-' self.style.li.spaceAfter = 0.275 * self.style.li.fontSize self.style.li.leading = 1.275 * self.style.li.fontSize
[docs] class IndexPdf(Pdf):
[docs] def adjust_style(self, font_size: int = 10) -> None: """ Adds styles for notices. """ super().adjust_style(font_size) self.style.index = deepcopy(self.style.normal) self.style.index.firstLineIndent = -2 * self.style.index.fontSize self.style.index.leftIndent = 2 * self.style.index.fontSize
[docs] def category_index(self, notices: 'GazetteNoticeCollection') -> None: """ Adds a category index. """ last_title: str | None = None categories = notices.session.query(Category) if notices.categories: categories = categories.filter(Category.id.in_(notices.categories)) categories = categories.order_by(Category.title) for category in categories: numbers = [] query = notices.query().with_entities(GazetteNotice._issues) query = query.filter( GazetteNotice._categories.has_key(category.name) # type:ignore ) for issues in query: numbers.extend([( IssueName.from_string(name).year, IssueName.from_string(name).number, int(number) ) for name, number in issues[0].items() if number]) formatted_numbers = ', '.join( f'{year}-{issue}-{number}' for year, issue, number in sorted(set(numbers)) ) if formatted_numbers: title = category.title if not last_title or last_title[0] != title[0]: self.h3(category.title[0]) last_title = category.title self.p_markup( f'{title}&nbsp;&nbsp;<i>{formatted_numbers}</i>', style=self.style.index )
[docs] def organization_index(self, notices: 'GazetteNoticeCollection') -> None: """ Adds an organization index. """ last_title: str | None = None organizations = notices.session.query(Organization) if notices.organizations: organizations = organizations.filter( Organization.id.in_(notices.organizations) ) organizations = organizations.order_by(Organization.title) for organization in organizations: numbers = [] query = notices.query().with_entities(GazetteNotice._issues) query = query.filter( GazetteNotice._organizations.has_key( # type:ignore organization.name ) ) for issues in query: numbers.extend([( IssueName.from_string(name).year, IssueName.from_string(name).number, int(number) ) for name, number in issues[0].items() if number]) formatted_numbers = ', '.join( f'{year}-{issue}-{number}' for year, issue, number in sorted(set(numbers)) ) if formatted_numbers: title = organization.title if not last_title or last_title[0] != title[0]: self.h3(organization.title[0]) last_title = organization.title self.p_markup( f'{title}&nbsp;&nbsp;<i>{formatted_numbers}</i>', style=self.style.index )
@classmethod
[docs] def from_notices( cls, notices: 'GazetteNoticeCollection', request: 'GazetteRequest' ) -> BytesIO: """ Create an index PDF from a collection of notices. """ title = request.translate(_('Gazette')) result = BytesIO() pdf = cls( result, title=title, author=request.app.principal.name ) pdf.init_a4_portrait( page_fn=page_fn_footer, page_fn_later=page_fn_header_and_footer ) pdf.h1(title) pdf.h1(request.translate(_('Index'))) pdf.h2(request.translate(_('Organizations'))) pdf.organization_index(notices) pdf.pagebreak() pdf.h2(request.translate(_('Categories'))) pdf.category_index(notices) pdf.generate() result.seek(0) return result
[docs] class NoticesPdf(Pdf):
[docs] def adjust_style(self, font_size: int = 10) -> None: """ Adds styles for notices. """ super().adjust_style(font_size) # Indent left everthing to stress the issue number self.style.leftIndent = 30 self.style.title.leftIndent = self.style.leftIndent self.style.heading1.leftIndent = self.style.leftIndent self.style.heading2.leftIndent = self.style.leftIndent self.style.heading3.leftIndent = self.style.leftIndent self.style.heading4.leftIndent = self.style.leftIndent self.style.paragraph.leftIndent = self.style.leftIndent self.style.ol.leftIndent = self.style.leftIndent self.style.ul.leftIndent = self.style.leftIndent
[docs] def notice( self, notice: GazetteNotice, layout: Layout, publication_number: int | str = 'xxx' ) -> None: """ Adds an official notice. """ if isinstance(publication_number, int): publication_number = str(publication_number) self.table( [[ MarkupParagraph(publication_number, self.style.normal), MarkupParagraph(notice.title, self.style.h_notice) ]], [self.style.leftIndent, None], style=self.style.table_h_notice ) self.story[-1].keepWithNext = True self.mini_html(notice.text or '') if notice.author_place and notice.author_date: self.story[-1].keepWithNext = True self.mini_html( '{}, {}<br>{}'.format( notice.author_place, layout.format_date( notice.author_date, 'date_long' ), notice.author_name ) ) for file in notice.files: self.pdf(file.reference.file)
@classmethod
[docs] def from_notice( cls, notice: GazetteNotice, request: 'GazetteRequest' ) -> BytesIO: """ Create a PDF from a single notice. """ layout = Layout(None, request) result = BytesIO() pdf = cls( result, author=request.app.principal.name ) pdf.init_a4_portrait( page_fn=page_fn_footer, page_fn_later=page_fn_header_and_footer ) pdf.spacer() pdf.notice(notice, layout) pdf.generate() result.seek(0) return result
@classmethod
[docs] def from_notices( cls, notices: 'GazetteNoticeCollection', request: 'GazetteRequest' ) -> BytesIO: """ Create a PDF from a collection of notices. """ layout = Layout(None, request) result = BytesIO() pdf = cls( result, author=request.app.principal.name ) pdf.init_a4_portrait( page_fn=page_fn_footer, page_fn_later=page_fn_header_and_footer ) for notice in notices.query(): pdf.spacer() pdf.notice(notice, layout) pdf.generate() result.seek(0) return result
[docs] class IssuePdf(NoticesPdf): """ A PDF containing all the notices of a single issue. Allows to automatically assign publication numbers when generating the PDF. """
[docs] def h(self, title: str, level: int = 0) -> None: """ Adds a title according to the given level. """ if not level: self.p_markup(title, self.style.title) else: getattr(self, f'h{min(level, 4)}')(title)
[docs] def notice( self, notice: GazetteNotice, layout: Layout, publication_number: int | str = 'xxx' ) -> None: """ Adds an official notice. Hides the content if it is print only. """ if notice.print_only: if isinstance(publication_number, int): publication_number = str(publication_number) title = layout.request.translate(_( 'This official notice is only available in the print version.' )) self.table( [[ MarkupParagraph(publication_number, self.style.normal), MarkupParagraph(f'<i>{title}</i>', self.style.normal) ]], [self.style.leftIndent, None], style=self.style.table_h_notice ) else: super().notice(notice, layout, publication_number)
[docs] def excluded_notices_note( self, number: int, request: 'GazetteRequest' ) -> None: """ Adds a paragraph with the number of excluded (print only) notices. """ note = _( 'The electronic official gazette is available at ' 'www.amtsblattzug.ch.' ) self.p_markup(request.translate(note), style=self.style.paragraph) if number: note = _( '${number} publication(s) with particularly sensitive data ' 'are not available online. They are available in paper form ' 'from the State Chancellery, Seestrasse 2, 6300 Zug, or can ' 'be subscribed to at amtsblatt@zg.ch.', mapping={'number': number} ) self.p_markup(request.translate(note), style=self.style.paragraph)
[docs] def unfold_data( self, session: 'Session', layout: Layout, issue: str, data: list[dict[str, Any]], publication_number: int | None, level: int = 1 ) -> int | None: """ Take a nested list of dicts and add it. """ for item in data: title = item.get('title', None) if title: self.h(title, level) self.story[-1].keepWithNext = True notices = item.get('notices', []) for id_ in notices: notice = session.query(GazetteNotice).filter_by(id=id_).one() if publication_number is None: self.notice(notice, layout, notice.issues[issue] or 'xxx') else: notice.set_publication_number(issue, publication_number) self.notice(notice, layout, publication_number) publication_number = publication_number + 1 children = item.get('children', []) if children: publication_number = self.unfold_data( session, layout, issue, children, publication_number, level + 1 ) if item.get('break_after', False): self.pagebreak() return publication_number
@staticmethod
[docs] def query_notices( session: 'Session', issue: str, organization: str, category: str ) -> list['UUID']: """ Queries all notices with the given values, ordered by publication number. """ notices = session.query( GazetteNotice.id ) notices = notices.filter( GazetteNotice._issues.has_key(issue), # type:ignore GazetteNotice.state == 'published', GazetteNotice._organizations.has_key(organization), # type:ignore GazetteNotice._categories.has_key(category) # type:ignore ) notices = notices.order_by( GazetteNotice._issues[issue], GazetteNotice.title ) return [notice for notice, in notices]
@classmethod
[docs] def query_used_categories( cls, session: 'Session', issue: 'Issue' ) -> set[str]: query = session.query(GazetteNotice._categories.keys()) # type:ignore query = query.filter( GazetteNotice._issues.has_key(issue.name), # type:ignore GazetteNotice.state == 'published', ) return {keys[0] for keys, in query if keys}
@classmethod
[docs] def query_used_organizations( cls, session: 'Session', issue: 'Issue' ) -> set[str]: query = session.query( GazetteNotice._organizations.keys() # type:ignore ) query = query.filter( GazetteNotice._issues.has_key(issue.name), # type:ignore GazetteNotice.state == 'published', ) return {keys[0] for keys, in query if keys}
@classmethod
[docs] def query_excluded_notices_count( cls, session: 'Session', issue: 'Issue' ) -> int: query = session.query(func.count(GazetteNotice.id)) query = query.filter( GazetteNotice._issues.has_key(issue.name), # type:ignore GazetteNotice.state == 'published', bool_is(GazetteNotice.meta['print_only'], True) ) return query.scalar()
@classmethod
[docs] def from_issue( cls, issue: 'Issue', request: 'GazetteRequest', first_publication_number: int | None, links: dict[str, str] | None = None ) -> BytesIO: """ Generate a PDF for one issue. Uses `first_publication_number` as a starting point for assigning publication numbers. Uses the existing numbers of the notices if None. """ # Collect the data data = [] session = request.session used_categories = cls.query_used_categories(session, issue) used_organizations = cls.query_used_organizations(session, issue) excluded_notices = cls.query_excluded_notices_count(session, issue) if used_categories and used_organizations: categories_q = session.query(Category) categories_q = categories_q.filter( Category.name.in_(used_categories) ) categories = categories_q.order_by(Category.order).all() roots = session.query(Organization).filter_by(parent_id=None) roots = roots.order_by(Organization.order) for root in roots: root_data: list[dict[str, Any]] = [] if not root.children: for category in categories: notices = cls.query_notices( session, issue.name, root.name, category.name ) if notices: root_data.append({ 'title': category.title, 'notices': notices }) else: for child in root.children: if child.name not in used_organizations: continue child_data = [] for category in categories: notices = cls.query_notices( session, issue.name, child.name, category.name ) if notices: child_data.append({ 'title': category.title, 'notices': notices }) if child_data: root_data.append({ 'title': child.title, 'children': child_data, 'break_after': True if child_data else False }) if root_data: data.append({ 'title': root.title, 'children': root_data }) # Generate the PDF layout = Layout(None, request) title = '{} {}'.format( request.translate(_('Gazette')), layout.format_issue(issue, date_format='date') ) file = BytesIO() pdf = cls( file, title=title, author=request.app.principal.name, logo=request.app.logo_for_pdf ) pdf.init_a4_portrait( page_fn=page_fn_header_logo_and_footer, page_fn_later=page_fn_header_and_footer ) pdf.h(title) pdf.excluded_notices_note(excluded_notices, request) pdf.unfold_data( session, layout, issue.name, data, first_publication_number ) # add a final page with links if links: if not isinstance(pdf.story[-1], PageBreak): pdf.pagebreak() pdf.h2(request.translate(_('Additional Links'))) html = '\n'.join( f'<p><b>{title}</b><br><a href="{url}">{url}</a></p>' for url, title in links.items() ) pdf.mini_html(html) pdf.generate() file.seek(0) return file
[docs] class IssuePrintOnlyPdf(IssuePdf): """ A PDF containing all the print only notices of a single issue. Generating this PDF does NOT assigns publication numbers! """
[docs] def notice( self, notice: GazetteNotice, layout: Layout, publication_number: str | int = 'xxx' ) -> None: """ Adds an official notice. """ if notice.print_only: # we skip the overriden implementation in IssuePdf super(IssuePdf, self).notice(notice, layout, publication_number)
[docs] def excluded_notices_note( self, number: int, request: 'GazetteRequest' ) -> None: """ Adds a paragraph with the number of excluded (print only) notices. """ if number: note = _( '${number} publication(s) with particularly sensitive data ' 'according to BGS 152.3 §7 Abs. 2.', mapping={'number': number} ) self.p_markup(request.translate(note), style=self.style.paragraph) note = _( 'The electronic official gazette is available at ' 'www.amtsblattzug.ch.' ) self.p_markup(request.translate(note), style=self.style.paragraph)
@staticmethod
[docs] def query_notices( session: 'Session', issue: str, organization: str, category: str ) -> list['UUID']: """ Queries all notices with the given values, ordered by publication number. """ notices = session.query( GazetteNotice.id ) notices = notices.filter( GazetteNotice._issues.has_key(issue), # type:ignore GazetteNotice.state == 'published', GazetteNotice._organizations.has_key(organization), # type:ignore GazetteNotice._categories.has_key(category), # type:ignore bool_is(GazetteNotice.meta['print_only'], True) ) notices = notices.order_by( GazetteNotice._issues[issue], GazetteNotice.title ) return [notice for notice, in notices]
@classmethod
[docs] def query_used_categories( cls, session: 'Session', issue: 'Issue' ) -> set[str]: query = session.query(GazetteNotice._categories.keys()) # type:ignore query = query.filter( GazetteNotice._issues.has_key(issue.name), # type:ignore GazetteNotice.state == 'published', bool_is(GazetteNotice.meta['print_only'], True) ) return {keys[0] for keys, in query if keys}
@classmethod
[docs] def query_used_organizations( cls, session: 'Session', issue: 'Issue' ) -> set[str]: query = session.query( GazetteNotice._organizations.keys() # type:ignore ) query = query.filter( GazetteNotice._issues.has_key(issue.name), # type:ignore GazetteNotice.state == 'published', bool_is(GazetteNotice.meta['print_only'], True) ) return {keys[0] for keys, in query if keys}
@classmethod
[docs] def from_issue( # type:ignore[override] cls, issue: 'Issue', request: 'GazetteRequest' ) -> BytesIO: """ Generate a PDF for one issue. """ return super().from_issue(issue, request, None)