""" 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
# meta bound values
[docs]
custom_css: 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]
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]
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
[docs]
custom_link_1_name: dict_property[str | None] = meta_property()
[docs]
custom_link_1_url: dict_property[str | None] = meta_property()
[docs]
custom_link_2_name: dict_property[str | None] = meta_property()
[docs]
custom_link_2_url: dict_property[str | None] = meta_property()
[docs]
custom_link_3_name: dict_property[str | None] = meta_property()
[docs]
custom_link_3_url: dict_property[str | None] = meta_property()
# 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()
[docs]
pdf_link_color: dict_property[str | None] = meta_property()
[docs]
pdf_underline_links: dict_property[bool] = meta_property(default=False)
# 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
# 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
# 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
@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)))