Source code for org.models.organisation

""" Contains the model describing the organisation proper. """

from datetime import date, timedelta
from functools import cached_property, lru_cache
from hashlib import sha256
from onegov.core.orm import Base
from onegov.core.orm.abstract import associated
from onegov.core.orm.mixins import (
    dict_markup_property, dict_property, meta_property, TimestampMixin)
from onegov.core.orm.types import JSON, UUID
from onegov.core.utils import linkify, paragraphify
from onegov.file.models.file import File
from onegov.form import flatten_fieldsets, parse_formcode
from onegov.org.theme import user_options
from onegov.org.models.tan import DEFAULT_ACCESS_WINDOW
from onegov.org.models.swiss_holidays import SwissHolidays
from sqlalchemy import Column, Text
from uuid import uuid4


from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
    import uuid
    from collections.abc import Iterator
    from markupsafe import Markup
    from onegov.form.parser.core import ParsedField
    from onegov.org.request import OrgRequest


[docs] class Organisation(Base, TimestampMixin): """ Defines the basic information associated with an organisation. It is assumed that there's only one organisation record in the schema! """
[docs] __tablename__ = 'organisations'
#: the id of the organisation, an automatically generated uuid
[docs] id: 'Column[uuid.UUID]' = Column( UUID, # type:ignore[arg-type] primary_key=True, default=uuid4 )
#: the name of the organisation
[docs] name: 'Column[str]' = Column(Text, nullable=False)
#: the logo of the organisation
[docs] logo_url: 'Column[str | None]' = Column(Text, nullable=True)
#: the theme options of the organisation
[docs] theme_options: 'Column[dict[str, Any] | None]' = Column( JSON, nullable=True, default=user_options.copy )
#: additional data associated with the organisation # FIXME: This should probably not be nullable
[docs] meta: 'Column[dict[str, Any]]' = Column( # type:ignore[assignment] JSON, nullable=True, default=dict )
# meta bound values
[docs] custom_css: dict_property[str | None] = meta_property()
[docs] contact: dict_property[str | None] = meta_property()
[docs] contact_url: dict_property[str | None] = meta_property()
[docs] opening_hours: dict_property[str | None] = meta_property()
[docs] opening_hours_url: dict_property[str | None] = meta_property()
[docs] about_url: dict_property[str | None] = meta_property()
[docs] reply_to: dict_property[str | None] = meta_property()
# FIXME: This is inherently unsafe, we should consider hard-coding # support for the few providers we need instead and only # allow users to select a provider and set the token(s) # and other configuration options available to that provider
[docs] analytics_code = dict_markup_property('meta')
[docs] online_counter_label: dict_property[str | None] = meta_property()
[docs] hide_online_counter: dict_property[bool | None] = meta_property()
[docs] reservations_label: dict_property[str | None] = meta_property()
[docs] hide_reservations: dict_property[bool | None] = meta_property()
[docs] publications_label: dict_property[str | None] = meta_property()
[docs] hide_publications: dict_property[bool | None] = meta_property()
[docs] event_limit_homepage: dict_property[int] = meta_property(default=3)
[docs] news_limit_homepage: dict_property[int] = meta_property(default=2)
[docs] focus_widget_image: dict_property[str | None] = meta_property()
[docs] daypass_label: dict_property[str | None] = meta_property()
[docs] e_move_label: dict_property[str | None] = meta_property()
[docs] e_move_url: dict_property[str | None] = meta_property()
[docs] default_map_view: dict_property[dict[str, Any] | None] = meta_property()
[docs] homepage_structure: dict_property[str | None] = meta_property()
[docs] homepage_cover = dict_markup_property('meta')
[docs] square_logo_url: dict_property[str | None] = meta_property()
# FIXME: really not a great name for this property considering # this is a single selection...
[docs] locales: dict_property[str | None] = meta_property()
[docs] redirect_homepage_to: dict_property[str | None] = meta_property()
[docs] redirect_path: dict_property[str | None] = meta_property()
[docs] hidden_people_fields: dict_property[list[str]] = meta_property( default=list )
[docs] event_locations: dict_property[list[str]] = meta_property(default=list)
[docs] geo_provider: dict_property[str] = meta_property(default='geo-mapbox')
[docs] holiday_settings: dict_property[dict[str, Any]] = meta_property( default=dict )
[docs] standard_image: dict_property[str | None] = meta_property()
[docs] submit_events_visible: dict_property[bool] = meta_property(default=True)
[docs] delete_past_events: dict_property[bool] = meta_property(default=False)
[docs] event_filter_type: dict_property[str] = meta_property(default='tags')
[docs] event_filter_definition: dict_property[str | None] = meta_property()
[docs] event_filter_configuration: dict_property[dict[str, Any]]
event_filter_configuration = meta_property(default=dict)
[docs] event_files = associated(File, 'event_files', 'many-to-many')
# social media
[docs] facebook_url: dict_property[str | None] = meta_property()
[docs] twitter_url: dict_property[str | None] = meta_property()
[docs] youtube_url: dict_property[str | None] = meta_property()
[docs] instagram_url: dict_property[str | None] = meta_property()
[docs] linkedin_url: dict_property[str | None] = meta_property()
[docs] tiktok_url: dict_property[str | None] = meta_property()
[docs] og_logo_default: dict_property[str | None] = meta_property()
# custom links # partner logos
[docs] partner_1_img: dict_property[str | None] = meta_property()
[docs] partner_1_url: dict_property[str | None] = meta_property()
[docs] partner_1_name: dict_property[str | None] = meta_property()
[docs] partner_2_img: dict_property[str | None] = meta_property()
[docs] partner_2_url: dict_property[str | None] = meta_property()
[docs] partner_2_name: dict_property[str | None] = meta_property()
[docs] partner_3_img: dict_property[str | None] = meta_property()
[docs] partner_3_url: dict_property[str | None] = meta_property()
[docs] partner_3_name: dict_property[str | None] = meta_property()
[docs] partner_4_img: dict_property[str | None] = meta_property()
[docs] partner_4_url: dict_property[str | None] = meta_property()
[docs] partner_4_name: dict_property[str | None] = meta_property()
[docs] always_show_partners: dict_property[bool] = meta_property(default=False)
# Ticket options
[docs] email_for_new_tickets: dict_property[str | None] = meta_property()
[docs] ticket_auto_accept_style: dict_property[str | None] = meta_property()
[docs] ticket_auto_accepts: dict_property[list[str] | None] = meta_property()
[docs] ticket_auto_accept_roles: dict_property[list[str] | None] = meta_property()
[docs] tickets_skip_opening_email: dict_property[list[str] | None]
tickets_skip_opening_email = meta_property()
[docs] tickets_skip_closing_email: dict_property[list[str] | None]
tickets_skip_closing_email = meta_property()
[docs] mute_all_tickets: dict_property[bool | None] = meta_property()
[docs] ticket_always_notify: dict_property[bool] = meta_property(default=True)
# username for the user supposed to automatically handle tickets
[docs] auto_closing_user: dict_property[str | None] = meta_property()
# Type boolean
[docs] report_changes: dict_property[bool | None] = meta_property()
# PDF rendering options
[docs] pdf_layout: dict_property[str | None] = meta_property()
# break points of pages after title of level x, type integer
[docs] page_break_on_level_root_pdf: dict_property[int | None] = meta_property()
[docs] page_break_on_level_org_pdf: dict_property[int | None] = meta_property()
# For custom search results or on the people detail view, include topmost # n levels as indexes of agency.ancestors, type: list of integers
[docs] agency_display_levels: dict_property[list[int] | None] = meta_property()
# Header settings that go into the div.globals
[docs] header_options: dict_property[dict[str, Any]] = meta_property(default=dict)
# Setting if show full agency path on people detail view
[docs] agency_path_display_on_people: dict_property[bool]
agency_path_display_on_people = meta_property(default=False) # Setting to index the last digits of the phone number as ES suggestion
[docs] agency_phone_internal_digits: dict_property[int | None] = meta_property()
[docs] agency_phone_internal_field: dict_property[str]
agency_phone_internal_field = meta_property(default='phone_direct') # Favicon urls for favicon macro
[docs] favicon_win_url: dict_property[str | None] = meta_property()
[docs] favicon_mac_url: dict_property[str | None] = meta_property()
[docs] favicon_apple_touch_url: dict_property[str | None] = meta_property()
[docs] favicon_pinned_tab_safari_url: dict_property[str | None] = meta_property()
# Links Settings
[docs] open_files_target_blank: dict_property[bool] = meta_property(default=True)
[docs] disable_page_refs: dict_property[bool] = meta_property(default=True)
# Footer column width settings
[docs] footer_left_width: dict_property[int] = meta_property(default=3)
[docs] footer_center_width: dict_property[int] = meta_property(default=5)
[docs] footer_right_width: dict_property[int] = meta_property(default=4)
# Newsletter settings
[docs] show_newsletter: dict_property[bool] = meta_property(default=False)
[docs] secret_content_allowed: dict_property[bool] = meta_property(default=False)
[docs] newsletter_categories: ( dict_property)[dict[str, list[dict[str, list[str]] | str]]] = ( meta_property(default=dict))
# Chat Settings
[docs] chat_staff: dict_property[list[str] | None] = meta_property()
[docs] enable_chat: dict_property[bool] = meta_property(default=False)
[docs] specific_opening_hours: dict_property[bool] = meta_property(default=False)
[docs] opening_hours_chat: dict_property[list[list[str]] | None] = meta_property()
[docs] chat_topics: dict_property[list[str] | None] = meta_property()
# Required information to upload documents to a Gever instance
[docs] gever_username: dict_property[str | None] = meta_property()
[docs] gever_password: dict_property[str | None] = meta_property()
[docs] gever_endpoint: dict_property[str | None] = meta_property()
# data retention policy
[docs] auto_archive_timespan: dict_property[int] = meta_property(default=0)
[docs] auto_delete_timespan: dict_property[int] = meta_property(default=0)
# MTAN Settings
[docs] mtan_access_window_seconds: dict_property[int | None] = meta_property()
[docs] mtan_access_window_requests: dict_property[int | None] = meta_property()
[docs] mtan_session_duration_seconds: dict_property[int | None] = meta_property()
# Open Data
[docs] ogd_publisher_mail: dict_property[str | None] = meta_property()
[docs] ogd_publisher_id: dict_property[str | None] = meta_property()
[docs] ogd_publisher_name: dict_property[str | None] = meta_property()
@property
[docs] def mtan_access_window(self) -> timedelta: seconds = self.mtan_access_window_seconds if seconds is None: return DEFAULT_ACCESS_WINDOW return timedelta(seconds=seconds)
@property
[docs] def mtan_session_duration(self) -> timedelta: seconds = self.mtan_session_duration_seconds if seconds is None: # by default we match it with the access window return self.mtan_access_window return timedelta(seconds=seconds)
@property
[docs] def public_identity(self) -> str: """ The public identity is a globally unique SHA 256 hash of the current organisation. Basically, this is the database record of the database, but mangled for security and because it is cooler 😎. This value can be accessed through /identity. """ return sha256(self.id.hex.encode('utf-8')).hexdigest()
@property
[docs] def holidays(self) -> SwissHolidays: """ Returns a SwissHolidays instance, as configured by the holiday_settings on the UI. """ return SwissHolidays( cantons=self.holiday_settings.get('cantons', ()), other=self.holiday_settings.get('other', ()) )
@property
[docs] def has_school_holidays(self) -> bool: """ Returns whether any school holidays have been configured """ return bool(self.holiday_settings.get('school', ()))
@property
[docs] def school_holidays(self) -> 'Iterator[tuple[date, date]]': """ Returns an iterable that yields date pairs of start and end dates of school holidays """ for y1, m1, d1, y2, m2, d2 in self.holiday_settings.get('school', ()): yield date(y1, m1, d1), date(y2, m2, d2)
@contact.setter # type:ignore[no-redef] def contact(self, value: str | None) -> None: self.meta['contact'] = value # update cache self.__dict__['contact_html'] = paragraphify(linkify(value)) @cached_property
[docs] def contact_html(self) -> 'Markup': return paragraphify(linkify(self.contact))
@opening_hours.setter # type:ignore[no-redef] def opening_hours(self, value: str | None) -> None: self.meta['opening_hours'] = value # update cache self.__dict__['opening_hours_html'] = paragraphify(linkify(value)) @cached_property
[docs] def opening_hours_html(self) -> 'Markup': return paragraphify(linkify(self.opening_hours))
@property
[docs] def title(self) -> str: return self.name.replace('|', ' ')
@property
[docs] def title_lines(self) -> tuple[str, str]: if '|' in self.name: parts = self.name.split('|') else: parts = self.name.split(' ') return ' '.join(parts[:-1]), parts[-1]
[docs] def excluded_person_fields(self, request: 'OrgRequest') -> list[str]: return [] if request.is_logged_in else self.hidden_people_fields
@property
[docs] def event_filter_fields(self) -> tuple['ParsedField', ...]: return flatten_event_filter_fields_from_definition( self.event_filter_definition)
@lru_cache(maxsize=64)
[docs] def flatten_event_filter_fields_from_definition( definition: str ) -> tuple['ParsedField', ...]: return tuple(flatten_fieldsets(parse_formcode(definition)))