import decimal import json from datetime import date, datetime, time from django.core.cache import cache from typing import Any, Dict, Optional import dateutil.parser from django.conf import settings from django.core.files import File from django.core.files.storage import default_storage from django.db.models import Model from django.utils.translation import ugettext_noop from i18nfield.strings import LazyI18nString from pretix.base.models.settings import GlobalSetting DEFAULTS = { 'max_items_per_order': { 'default': '10', 'type': int }, 'display_net_prices': { 'default': 'False', 'type': bool }, 'attendee_names_asked': { 'default': 'True', 'type': bool }, 'attendee_names_required': { 'default': 'False', 'type': bool }, 'invoice_address_asked': { 'default': 'True', 'type': bool, }, 'invoice_address_required': { 'default': 'False', 'type': bool, }, 'invoice_address_vatid': { 'default': 'False', 'type': bool, }, 'invoice_numbers_consecutive': { 'default': 'True', 'type': bool, }, 'reservation_time': { 'default': '30', 'type': int }, 'payment_term_days': { 'default': '14', 'type': int }, 'payment_term_last': { 'default': None, 'type': datetime, }, 'payment_term_weekdays': { 'default': 'True', 'type': bool }, 'payment_term_expire_automatically': { 'default': 'True', 'type': bool }, 'payment_term_accept_late': { 'default': 'True', 'type': bool }, 'presale_start_show_date': { 'default': 'True', 'type': bool }, 'tax_rate_default': { 'default': '0.00', 'type': decimal.Decimal }, 'invoice_generate': { 'default': 'False', 'type': str }, 'invoice_address_from': { 'default': '', 'type': str }, 'invoice_introductory_text': { 'default': '', 'type': LazyI18nString }, 'invoice_additional_text': { 'default': '', 'type': LazyI18nString }, 'invoice_footer_text': { 'default': '', 'type': LazyI18nString }, 'invoice_language': { 'default': '__user__', 'type': str }, 'show_items_outside_presale_period': { 'default': 'True', 'type': bool }, 'timezone': { 'default': settings.TIME_ZONE, 'type': str }, 'locales': { 'default': json.dumps([settings.LANGUAGE_CODE]), 'type': list }, 'locale': { 'default': settings.LANGUAGE_CODE, 'type': str }, 'show_date_to': { 'default': 'True', 'type': bool }, 'show_times': { 'default': 'True', 'type': bool }, 'show_quota_left': { 'default': 'False', 'type': bool }, 'show_variations_expanded': { 'default': 'False', 'type': bool }, 'waiting_list_enabled': { 'default': 'False', 'type': bool }, 'waiting_list_auto': { 'default': 'True', 'type': bool }, 'waiting_list_hours': { 'default': '48', 'type': int }, 'ticket_download': { 'default': 'False', 'type': bool }, 'ticket_download_date': { 'default': None, 'type': datetime }, 'last_order_modification_date': { 'default': None, 'type': datetime }, 'cancel_allow_user': { 'default': 'True', 'type': bool }, 'contact_mail': { 'default': None, 'type': str }, 'imprint_url': { 'default': None, 'type': str }, 'mail_prefix': { 'default': None, 'type': str }, 'mail_from': { 'default': settings.MAIL_FROM, 'type': str }, 'mail_text_resend_link': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, you receive this message because you asked us to send you the link to your order for {event}. You can change your order details and view the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_text_resend_all_links': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, somebody requested a list of your orders for {event}. The list is as follows: {orders} Best regards, Your {event} team""")) }, 'mail_text_order_free': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, we successfully received your order for {event}. As you only ordered free products, no payment is required. You can change your order details and view the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_text_order_placed': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, we successfully received your order for {event} with a total value of {total} {currency}. Please complete your payment before {date}. {paymentinfo} You can change your order details and view the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_text_order_changed': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, your order for {event} has been changed. You can view the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_text_order_paid': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, we successfully received your payment for {event}. Thank you! {payment_info} You can change your order details and view the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_days_order_expire_warning': { 'type': int, 'default': '3' }, 'mail_text_order_expire_warning': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, we did not yet receive a payment for your order for {event}. Please keep in mind that if we only guarantee your order if we receive your payment before {expire_date}. You can view the payment information and the status of your order at {url} Best regards, Your {event} team""")) }, 'mail_text_waiting_list': { 'type': LazyI18nString, 'default': LazyI18nString.from_gettext(ugettext_noop("""Hello, you submitted yourself to the waiting list for {event}, for the product {product}. We now have a ticket ready for you! You can redeem it in our ticket shop within the next {hours} hours by entering the following voucher code: {code} Alternatively, you can just click on the following link: {url} Please note that this link is only valid within the next {hours} hours! We will reassign the ticket to the next person on the list if you do not redeem the voucher within that timeframe. Best regards, Your {event} team""")) }, 'smtp_use_custom': { 'default': 'False', 'type': bool }, 'smtp_host': { 'default': '', 'type': str }, 'smtp_port': { 'default': 587, 'type': int }, 'smtp_username': { 'default': '', 'type': str }, 'smtp_password': { 'default': '', 'type': str }, 'smtp_use_tls': { 'default': 'True', 'type': bool }, 'smtp_use_ssl': { 'default': 'False', 'type': bool }, 'primary_color': { 'default': '#8E44B3', 'type': str }, 'presale_css_file': { 'default': None, 'type': str }, 'presale_css_checksum': { 'default': None, 'type': str }, 'logo_image': { 'default': None, 'type': File }, 'invoice_logo_image': { 'default': None, 'type': File }, 'frontpage_text': { 'default': '', 'type': LazyI18nString }, 'update_check_ack': { 'default': 'False', 'type': bool }, 'update_check_email': { 'default': '', 'type': str }, 'update_check_perform': { 'default': 'True', 'type': bool }, 'update_check_result': { 'default': None, 'type': dict }, 'update_check_result_warning': { 'default': 'False', 'type': bool }, 'update_check_last': { 'default': None, 'type': datetime }, 'update_check_id': { 'default': None, 'type': str } } class SettingsProxy: """ This object allows convenient access to settings stored in the EventSettings/OrganizerSettings database model. It exposes all settings as properties and it will do all the nasty inheritance and defaults stuff for you. """ def __init__(self, obj: Model, parent: Optional[Model]=None, type=None): self._obj = obj self._parent = parent self._cached_obj = None self._write_cached_obj = None self._type = type def _cache(self) -> Dict[str, Any]: if self._cached_obj is None: self._cached_obj = cache.get_or_set( 'settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk), lambda: {s.key: s.value for s in self._obj.setting_objects.all()}, timeout=1800 ) return self._cached_obj def _write_cache(self) -> Dict[str, Any]: if self._write_cached_obj is None: self._write_cached_obj = { s.key: s for s in self._obj.setting_objects.all() } return self._write_cached_obj def _flush(self) -> None: self._cached_obj = None self._write_cached_obj = None self._flush_external_cache() def _flush_external_cache(self): cache.delete('settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk)) def freeze(self) -> dict: """ Returns a dictionary of all settings set for this object, including any default values of its parents or hardcoded in pretix. """ settings = {} for key, v in DEFAULTS.items(): settings[key] = self._unserialize(v['default'], v['type']) if self._parent: settings.update(self._parent.settings.freeze()) for key in self._cache(): settings[key] = self.get(key) return settings def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any: if as_type is None and value is not None and value.startswith('file://'): as_type = File if as_type is not None and isinstance(value, as_type): return value elif value is None: return None elif as_type == int or as_type == float or as_type == decimal.Decimal: return as_type(value) elif as_type == dict or as_type == list: return json.loads(value) elif as_type == bool or value in ('True', 'False'): return value == 'True' elif as_type == File: try: fi = default_storage.open(value[7:], 'rb' if binary_file else 'r') fi.url = default_storage.url(value[7:]) return fi except OSError: return False elif as_type == datetime: return dateutil.parser.parse(value) elif as_type == date: return dateutil.parser.parse(value).date() elif as_type == time: return dateutil.parser.parse(value).time() elif as_type == LazyI18nString and not isinstance(value, LazyI18nString): try: return LazyI18nString(json.loads(value)) except ValueError: return LazyI18nString(str(value)) elif as_type is not None and issubclass(as_type, Model): return as_type.objects.get(pk=value) return value def _serialize(self, value: Any) -> str: if isinstance(value, str): return value elif isinstance(value, int) or isinstance(value, float) \ or isinstance(value, bool) or isinstance(value, decimal.Decimal): return str(value) elif isinstance(value, list) or isinstance(value, dict): return json.dumps(value) elif isinstance(value, datetime) or isinstance(value, date) or isinstance(value, time): return value.isoformat() elif isinstance(value, Model): return value.pk elif isinstance(value, LazyI18nString): return json.dumps(value.data) elif isinstance(value, File): return 'file://' + value.name raise TypeError('Unable to serialize %s into a setting.' % str(type(value))) def get(self, key: str, default=None, as_type: type=None, binary_file=False): """ Get a setting specified by key ``key``. Normally, settings are strings, but if you put non-strings into the settings object, you can request unserialization by specifying ``as_type``. If the key does not have a harcdoded type in the pretix source, omitting ``as_type`` always will get you a string. If the setting with the specified name does not exist on this object, any parent object will be queried (e.g. the organizer of an event). If still no value is found, a default value hardcoded will be returned if one exists. If not, the value of the ``default`` argument will be returned instead. """ if as_type is None and key in DEFAULTS: as_type = DEFAULTS[key]['type'] if key in self._cache(): value = self._cache()[key] else: value = None if self._parent: value = self._parent.settings.get(key, as_type=str) if value is None and key in DEFAULTS: value = DEFAULTS[key]['default'] if value is None and default is not None: value = default return self._unserialize(value, as_type, binary_file=binary_file) def __getitem__(self, key: str) -> Any: return self.get(key) def __getattr__(self, key: str) -> Any: if key.startswith('_'): return super().__getattr__(key) return self.get(key) def __setattr__(self, key: str, value: Any) -> None: if key.startswith('_'): return super().__setattr__(key, value) self.set(key, value) def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) def set(self, key: str, value: Any) -> None: """ Stores a setting to the database of its object. """ wc = self._write_cache() if key in wc: s = wc[key] else: s = self._type(object=self._obj, key=key) s.value = self._serialize(value) s.save() self._cache()[key] = s.value wc[key] = s self._flush_external_cache() def __delattr__(self, key: str) -> None: if key.startswith('_'): return super().__delattr__(key) self.delete(key) def __delitem__(self, key: str) -> None: self.delete(key) def delete(self, key: str) -> None: """ Deletes a setting from this object's storage. """ if key in self._write_cache(): self._write_cache()[key].delete() del self._write_cache()[key] if key in self._cache(): del self._cache()[key] self._flush_external_cache() class SettingsSandbox: """ Transparently proxied access to event settings, handling your prefixes for you. :param typestr: The first part of the pretix, e.g. ``plugin`` :param key: The prefix, e.g. the name of your plugin :param obj: The event or organizer that should be queried """ def __init__(self, typestr: str, key: str, obj: Model): self._event = obj self._type = typestr self._key = key def _convert_key(self, key: str) -> str: return '%s_%s_%s' % (self._type, self._key, key) def __setitem__(self, key: str, value: Any) -> None: self.set(key, value) def __setattr__(self, key: str, value: Any) -> None: if key.startswith('_'): return super().__setattr__(key, value) self.set(key, value) def __getattr__(self, item: str) -> Any: return self.get(item) def __getitem__(self, item: str) -> Any: return self.get(item) def __delitem__(self, key: str) -> None: del self._event.settings[self._convert_key(key)] def __delattr__(self, key: str) -> None: del self._event.settings[self._convert_key(key)] def get(self, key: str, default: Any=None, as_type: type=str): return self._event.settings.get(self._convert_key(key), default=default, as_type=as_type) def set(self, key: str, value: Any): self._event.settings.set(self._convert_key(key), value) class GlobalSettingsObject(): settings_namespace = 'global' def __init__(self): self.settings = SettingsProxy(self, type=GlobalSetting) self.setting_objects = GlobalSetting.objects self.slug = '_global' self.pk = '_global'