Source code for assembl.models.preferences

# -*- coding: utf-8 -*-
"""A set of preferences that apply to a Discussion.

May be defined at the user, Discussion or server level."""
from future import standard_library
from future.utils import text_type
standard_library.install_aliases()
from builtins import str
from itertools import chain
from collections import MutableMapping

from future.utils import string_types
import simplejson as json
from sqlalchemy import (
    Column,
    Integer,
    Text,
    Unicode,
    ForeignKey,
)
from sqlalchemy.orm import relationship
from ..lib.sqla_types import CoerceUnicode
from pyramid.httpexceptions import HTTPUnauthorized

from . import AbstractBase, NamedClassMixin
from ..auth import *
from ..lib.abc import classproperty
from ..lib.locale import _, strip_country
from ..lib import config


def merge_json(base, patch):
    # simplistic recursive dictionary merge
    if not (isinstance(base, dict) and isinstance(patch, dict)):
        return patch
    base = dict(base)
    for k, v in patch.items():
        if k in base:
            base[k] = merge_json(base[k], v)
        else:
            base[k] = v
    return base


[docs]class Preferences(MutableMapping, NamedClassMixin, AbstractBase): """ Cascading preferences """ __tablename__ = "preferences" BASE_PREFS_NAME = "default" id = Column(Integer, primary_key=True) name = Column(CoerceUnicode, nullable=False, unique=True) cascade_id = Column(Integer, ForeignKey(id), nullable=True) pref_json = Column("values", Text()) # JSON blob cascade_preferences = relationship("Preferences", remote_side=[id]) @classmethod def get_naming_column_name(self): return "name" @classmethod def get_by_name(cls, name=None, session=None): name = name or cls.BASE_PREFS_NAME session = session or cls.default_db return session.query(cls).filter_by(name=name).first() @classmethod def get_default_preferences(cls, session=None): return cls.get_by_name('default', session) or cls(name='default') @classmethod def get_discussion_conditions(cls, discussion_id): # This is not a DiscussionBoundBase, but protocol is otherwise useful from .discussion import Discussion return ((cls.id == Discussion.preferences_id), (Discussion.id == discussion_id))
[docs] @classmethod def init_from_settings(cls, settings): """Initialize some preference values""" from ..auth.social_auth import get_active_auth_strategies # TODO: Give linguistic names to social auth providers. active_strategies = { k: k for k in get_active_auth_strategies(settings)} active_strategies[''] = _("No special authentication") cls.preference_data['authorization_server_backend']['scalar_values'] = active_strategies
@property def local_values_json(self): values = {} if self.pref_json: values = json.loads(self.pref_json) assert isinstance(values, dict) return values @local_values_json.setter def local_values_json(self, val): assert isinstance(val, dict) self.pref_json = json.dumps(val) @property def values_json(self): if self.cascade_preferences: values = self.cascade_preferences.values_json else: values = self.property_defaults if not values.get('preference_data', None): values['preference_data'] = self.get_preference_data_list() values.update(self.local_values_json) return values def safe_local_values_json(self, permissions): json = self.local_values_json spec = self.get_preference_data() json = {k: v for (k, v) in json.items() if k in spec} permissions = permissions[:] or [] permissions.append("default_read") json = {k: v for (k, v) in json.items() if k in spec and spec[k].get("read_permission", "default_read") in permissions} return json def can_read(self, key, permissions): specs = self.get_preference_data() spec = specs.get(key, None) if not spec: return False needed = spec.get("read_permission", None) return not needed or (needed in permissions) def can_modify(self, key, permissions): specs = self.get_preference_data() spec = specs.get(key, None) if not spec: return False needed = spec.get("modification_permission", None) return not needed or (needed in permissions) def safe_get_value(self, key, permissions): specs = self.get_preference_data() spec = specs.get(key, None) if spec: needed = spec.get("read_permission", None) if not needed or (needed in permissions): return self[key] def safe_property_defaults(self, permissions): values = self.property_defaults if not values.get('preference_data', None): values['preference_data'] = self.get_preference_data_list() spec = self.get_preference_data() permissions = permissions[:] permissions.append("default_read") return {k: v for (k, v) in values.items() if k in spec and spec[k].get("read_permission", "default_read") in permissions} def safe_values_json(self, permissions): if self.cascade_preferences: values = self.cascade_preferences.safe_values_json(permissions) else: values = self.safe_property_defaults(permissions) values.update(self.safe_local_values_json(permissions)) return values def _get_local(self, key): if key not in self.preference_data: raise KeyError("Unknown preference: " + key) values = self.local_values_json if key in values: value = values[key] return True, value return False, None def get_local(self, key): exists, value = self._get_local(key) if exists: return value def __getitem__(self, key): if key == 'name': return self.name if key == '@extends': return (self.uri_generic(self.cascade_id) if self.cascade_id else None) exists, value = self._get_local(key) if exists: return value elif self.cascade_id: return self.cascade_preferences[key] if key == "preference_data": return self.get_preference_data_list() return self.get_preference_data()[key].get("default", None) def __len__(self): return len(self.preference_data_list) + 2 def __iter__(self): return chain(self.preference_data_key_list, ( 'name', '@extends')) def __contains__(self, key): return key in self.preference_data_key_set def __delitem__(self, key): values = self.local_values_json if key in values: oldval = values[key] del values[key] self.local_values_json = values return oldval def __setitem__(self, key, value): if key == 'name': old_value = self.name self.name = text_type(value) return old_value elif key == '@extends': old_value = self.get('@extends') new_pref = self.get_instance(value) if new_pref is None: raise KeyError("Does not exist:" + value) self.cascade_preferences = new_pref return old_value if key not in self.preference_data_key_set: raise KeyError("Unknown preference: " + key) values = self.local_values_json old_value = values.get(key, None) value = self.validate(key, value) values[key] = value self.local_values_json = values return old_value def can_edit(self, key, permissions=(P_READ,), pref_data=None): if P_SYSADMIN in permissions: if key == 'name' and self.name == self.BASE_PREFS_NAME: # Protect the base name return False return True if key in ('name', '@extends', 'preference_data'): # TODO: Delegate permissions. return False if key not in self.preference_data_key_set: raise KeyError("Unknown preference: " + key) if pref_data is None: pref_data = self.get_preference_data()[key] req_permission = pref_data.get( 'modification_permission', P_ADMIN_DISC) if req_permission not in permissions: return False return True def safe_del(self, key, permissions=(P_READ,)): if not self.can_edit(key, permissions): raise HTTPUnauthorized("Cannot delete "+key) del self[key] def safe_set(self, key, value, permissions=(P_READ,)): if not self.can_edit(key, permissions): raise HTTPUnauthorized("Cannot edit "+key) self[key] = value def validate(self, key, value, pref_data=None): if pref_data is None: pref_data = self.get_preference_data()[key] validator = pref_data.get('backend_validator_function', None) if validator: # This has many points of failure, but all failures are meaningful. module, function = validator.rsplit(".", 1) from importlib import import_module mod = import_module(module) try: value = getattr(mod, function)(value) if value is None: raise ValueError("Empty value after validation") except Exception as e: raise ValueError(e.message) data_type = pref_data.get("value_type", "json") return self.validate_single_value(key, value, pref_data, data_type) def validate_single_value(self, key, value, pref_data, data_type): # TODO: Validation for the datatypes. # base_type: (bool|json|int|string|text|scalar|url|email|domain|locale|langstr|permission|role|pubflow|pubstate|password) # type: base_type|list_of_(type)|dict_of_(base_type)_to_(type) if data_type.startswith("list_of_"): assert isinstance(value, (list, tuple)), "Not a list" return [ self.validate_single_value(key, val, pref_data, data_type[8:]) for val in value] elif data_type.startswith("dict_of_"): assert isinstance(value, (dict)), "Not a dict" key_type, value_type = data_type[8:].split("_to_", 1) assert "_" not in key_type return { self.validate_single_value(key, k, pref_data, key_type): self.validate_single_value(key, v, pref_data, value_type) for (k, v) in value.items()} elif data_type == "langstr": # Syntactic sugar for dict_of_locale_to_string assert isinstance(value, (dict)), "Not a dict" return { self.validate_single_value(key, k, pref_data, "locale"): self.validate_single_value(key, v, pref_data, "string") for (k, v) in value.items()} elif data_type == "bool": assert isinstance(value, bool), "Not a boolean" elif data_type == "int": assert isinstance(value, int), "Not an integer" elif data_type == "json": pass # no check else: assert isinstance(value, string_types), "Not a string" if data_type in ("string", "text", "password"): pass elif data_type == "scalar": assert value in pref_data.get("scalar_values", ()), ( "value not allowed: " + value) elif data_type == "url": from urllib.parse import urlparse assert urlparse(value).scheme in ( 'http', 'https'), "Not a HTTP URL" elif data_type == "email": from pyisemail import is_email assert is_email(value), "Not an email" elif data_type == "locale": pass # TODO elif data_type == "pubflow": from .publication_states import PublicationFlow assert PublicationFlow.getByName(value) elif data_type == "pubstate": discussion = self.discussion if discussion: idea_pub_flow = discussion.idea_publication_flow else: from .publication_states import PublicationFlow idea_pub_flow = PublicationFlow.getByName(self['default_idea_pub_flow']) assert idea_pub_flow, "No flow available" assert idea_pub_flow.state_by_label(value), "No state %d in flow %d" % ( value, idea_pub_flow.label) elif data_type == "permission": assert value in ASSEMBL_PERMISSIONS elif data_type == "role": if value not in SYSTEM_ROLES: from .auth import Role assert self.db.query(Role).filter_by( name=value).count() == 1, "Unknown role" elif data_type == "domain": from pyisemail.validators.dns_validator import DNSValidator v = DNSValidator() assert v.is_valid(value), "Not a valid domain" value = value.lower() else: raise RuntimeError("Invalid data_type: " + data_type) return value
[docs] def generic_json( self, view_def_name='default', user_id=Everyone, permissions=(P_READ, ), base_uri='local:'): # TODO: permissions values = self.local_values_json values['name'] = self.name if self.cascade_preferences: values['@extends'] = self.cascade_preferences.name values['@id'] = self.uri() return values
def _do_update_from_json( self, json, parse_def, context, duplicate_handling=None, object_importer=None): for key, value in json.items(): if key == '@id': if value != self.uri(): raise RuntimeError("Wrong id") else: self[key] = value return self def __hash__(self): return AbstractBase.__hash__(self) @classproperty def property_defaults(cls): return {p['id']: p.get("default", None) for p in cls.preference_data_list} def get_preference_data(self): if self.cascade_id: base = self.cascade_preferences.get_preference_data() else: base = self.preference_data exists, patch = self._get_local("preference_data") if exists: base = merge_json(base, patch) return base def get_preference_data_list(self): data = self.get_preference_data() keys = self.preference_data_key_list return [data[key] for key in keys] crud_permissions = CrudPermissions(P_SYSADMIN) # This defines the allowed properties and their data format # Each preference metadata has the following format: # id (the key for the preference as a dictionary) # name (for interface) # description (for interface, hover help) # value_type: given by the following grammar: # base_type = (bool|json|int|string|text|scalar|url|email|domain|locale|langstr|permission|role) # type = base_type|list_of_(type)|dict_of_(base_type)_to_(type) # more types may be added, but need to be added to both frontend and backend # show_in_preferences: Do we always hide this preference? # modification_permission: What permission do you need to change that preference? # (default: P_DISCUSSION_ADMIN) # allow_user_override: Do we allow users to have their personal value for that permission? # if so what permission is required? (default False) # scalar_values: "{value: "label"}" a dictionary of permitted options for a scalar value type # default: the default value # item_default: the default value for new items in a list_of_T... or dict_of_BT_to_T... preference_data_list = [ # Languages used in the discussion. { "id": "preferred_locales", "value_type": "list_of_locale", "name": _("Languages used"), "description": _("All languages expected in the discussion"), "allow_user_override": None, "item_default": "en", "default": [strip_country(x) for x in config.get_config().get( 'available_languages', 'fr en').split()] }, # full class name of translation service to use, if any # e.g. assembl.nlp.translate.GoogleTranslationService { "id": "translation_service", "name": _("Translation service"), "value_type": "scalar", "scalar_values": { "": _("No translation"), "assembl.nlp.translation_service.DummyTranslationServiceTwoSteps": _("Dummy translation service (two steps)"), "assembl.nlp.translation_service.DummyTranslationServiceOneStep": _("Dummy translation service (one step)"), "assembl.nlp.translation_service.DummyTranslationServiceTwoStepsWithErrors": _("Dummy translation service (two steps) with errors"), "assembl.nlp.translation_service.DummyTranslationServiceOneStepWithErrors": _("Dummy translation service (one step) with errors"), "assembl.nlp.translation_service.GoogleTranslationService": _("Google Translate"), "assembl.nlp.translation_service.DeeplTranslationService": _("Deepl"), }, "description": _( "Translation service"), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "" }, # full class name of translation service to use, if any # e.g. assembl.nlp.translate.GoogleTranslationService { "id": "translation_service_api_key", "name": _("Translation service API key"), "value_type": "password", "description": _( "API key for translation service"), "allow_user_override": None, "modification_permission": P_SYSADMIN, "read_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "" }, # Simple view panel order, eg NIM or NMI { "id": "simple_view_panel_order", "name": _("Panel order in simple view"), "value_type": "scalar", "scalar_values": { "NIM": _("Navigation, Idea, Messages"), "NMI": _("Navigation, Messages, Idea")}, "description": _("Order of panels"), "allow_user_override": P_READ, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "NMI" }, # Allow social sharing { "id": "social_sharing", "name": _("Social sharing"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _("Show the share button on posts and ideas"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": True }, # Require virus check { "id": "requires_virus_check", "name": _("Require anti-virus check"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _("Run an anti-virus on file attachments"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": False # for development }, # Terms of service version { "id": "tos_version", "name": _("Terms of service version"), "value_type": "int", # "scalar_values": {value: "label"}, "description": _("Version number of terms of service. Increment when terms change, participants will be alerted."), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": 1 }, # Terms of service version { "id": "terms_of_service", "name": _("Terms of service"), "value_type": "dict_of_locale_to_text", # "scalar_values": {value: "label"}, "description": _("Terms of service. Multilingual HTML String."), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": None # for development }, { "id": "authorization_server_backend", "value_type": "scalar", "scalar_values": { "": _("No special authentication"), }, "name": _( "Authentication service type"), "description": _( "A primary authentication server for this discussion, defined " "as a python-social-auth backend. Participants will be " "auto-logged in to that server, and discussion auto-" "subscription will require an account from this backend."), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "" }, # Are moderated posts simply hidden or made inaccessible by default? { "id": "default_allow_access_to_moderated_text", "name": _("Allow access to moderated text"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _( "Are moderated posts simply hidden or made inaccessible " "by default?"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": True }, # Does the Idea panel automatically open when an idea is clicked? (and close when a special section is clicked) { "id": "idea_panel_opens_automatically", "name": _("Idea panel opens automatically"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _( "Does the Idea panel automatically open when an idea is clicked ? (and close when a special section is clicked)"), "allow_user_override": P_READ, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": True }, # The specification of the default idea publication flow for a discussion { "id": "default_idea_pub_flow", "name": _("Default idea publication flow"), "value_type": "pubflow", "show_in_preferences": False, "description": _( "The idea publication flow to use for a new discussion"), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "default" }, # The specification of the default idea publication state for new ideas { "id": "default_idea_pub_state", "name": _("Publication state of a new idea"), "value_type": "pubstate", "scalar_values": [], "description": _( "The idea publication state to use for a new ideas, taken from the discussion's flow"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "shared" }, # The specification of the default permissions for a discussion { "id": "default_permissions", "name": _("Default private permissions"), "value_type": "dict_of_role_to_list_of_permission", "show_in_preferences": False, "description": _( "The base permissions for a new private discussion"), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "item_default": { R_PARTICIPANT: [P_READ], }, "default": { R_ADMINISTRATOR: [ P_ADD_EXTRACT, P_ADD_IDEA, P_ADD_POST, P_ADMIN_DISC, P_DELETE_POST, P_DISC_STATS, P_EDIT_EXTRACT, P_EDIT_IDEA, P_ASSOCIATE_IDEA, P_EDIT_POST, P_EDIT_SYNTHESIS, P_EXPORT_EXTERNAL_SOURCE, P_MODERATE, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_SEND_SYNTHESIS, P_VOTE, ], R_CATCHER: [ P_ADD_EXTRACT, P_ADD_IDEA, P_ADD_POST, P_EDIT_EXTRACT, P_EDIT_IDEA, P_ASSOCIATE_IDEA, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_VOTE, ], R_MODERATOR: [ P_ADD_EXTRACT, P_ADD_IDEA, P_ADD_POST, P_DELETE_POST, P_DISC_STATS, P_EDIT_EXTRACT, P_EDIT_IDEA, P_ASSOCIATE_IDEA, P_EDIT_POST, P_EDIT_SYNTHESIS, P_EXPORT_EXTERNAL_SOURCE, P_MODERATE, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_SEND_SYNTHESIS, P_VOTE, ], R_PARTICIPANT: [ P_ADD_POST, P_READ_USER_INFO, P_VOTE, P_READ, P_READ_IDEA, ], R_OWNER: [ P_DELETE_POST, P_EDIT_EXTRACT, ], }, }, # The specification of the default permissions for a public discussion { "id": "default_permissions_public", "name": _("Default public permissions"), "value_type": "dict_of_role_to_list_of_permission", "show_in_preferences": False, "description": _( "Extra permissions for a new public discussion"), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "item_default": { R_PARTICIPANT: [P_READ], }, "default": { Authenticated: [ P_SELF_REGISTER, ], Everyone: [ P_READ, P_READ_IDEA, ], }, }, # Registration requires being a member of this email domain. { "id": "require_email_domain", "name": _("Require Email Domain"), "value_type": "list_of_domain", # "scalar_values": {value: "label"}, "description": _( "List of domain names of user email address required for " "self-registration. Only accounts with at least an email from those " "domains can self-register to this discussion. Anyone can " "self-register if this is empty."), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": [], "item_default": "" }, # Show the CI Dashboard in the panel group window { "id": "show_ci_dashboard", "name": _("Show CI Dashboard"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _( "Show the CI Dashboard in the panel group window"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": False }, # Idea and link types { "id": "idea_typology", "name": _("Idea Typology"), "value_type": "json", "description": _( "Idea types, must be present in ontology"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": {} }, # Extra CSS { "id": "extra_css", "name": _("Extra CSS"), "value_type": "text", "description": _("CSS"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "" }, # Configuration of the visualizations shown in the CI Dashboard { "id": "ci_dashboard_url", "name": _("URL of CI Dashboard"), "value_type": "url", "description": _( "Configuration of the visualizations shown in the " "CI Dashboard"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": "//cidashboard.net/ui/visualisations/index.php?" "width=1000&height=1000&vis=11,23,p22,13,p7,7,12,p2,p15,p9," "p8,p1,p10,p14,5,6,16,p16,p17,18,p20,p4&lang=<%= lang %>" "&title=&url=<%= url %>&userurl=<%= user_url %>" "&langurl=&timeout=60" }, # List of visualizations { "id": "visualizations", "name": _("Catalyst Visualizations"), "value_type": "json", # "scalar_values": {value: "label"}, "description": _( "A JSON description of available Catalyst visualizations"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": {} }, # Extra navigation sections (refers to visualizations) { "id": "navigation_sections", "name": _("Navigation sections"), "value_type": "json", # "scalar_values": {value: "label"}, "description": _( "A JSON specification of Catalyst visualizations to show " "in the navigation section"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": {} }, # Translations for the navigation sections { "id": "translations", "name": _("Catalyst translations"), "value_type": "json", # "scalar_values": {value: "label"}, "description": _( "Translations applicable to Catalyst visualizations, " "in Jed (JSON) format"), "allow_user_override": None, # "view_permission": P_READ, # by default "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": {} }, # Default expanded/collapsed state of each idea in the table of ideas. # A user can override it by opening/closing an idea. # This is a hash where keys are ideas ids, and values are booleans. # We could use dict_of_string_to_bool, but that would clutter the interface. { "id": "default_table_of_ideas_collapsed_state", "name": _("Default Table of Ideas Collapsed state"), "value_type": "json", # "scalar_values": {value: "label"}, "description": _( "Default expanded/collapsed state of each idea in the table " "of ideas. A user can override it by opening/closing an idea"), "allow_user_override": None, # "view_permission": P_READ, # by default "modification_permission": P_ADD_IDEA, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": {}, "show_in_preferences": False }, # The specification of the preference data { "id": "preference_data", "name": _("Preference data"), "value_type": "json", "show_in_preferences": False, "description": _( "The preference configuration; override only with care"), "allow_user_override": None, "modification_permission": P_SYSADMIN, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": None # this should be recursive... }, # The specification of the cookies banner { "id": "cookies_banner", "name": _("Cookies banner"), "value_type": "bool", "show_in_preferences": True, "description": _( "Show the banner offering to disable Piwik cookies"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": True # this should be recursive... } ] # Precompute, this is not mutable. preference_data_key_list = [p["id"] for p in preference_data_list] preference_data_key_set = set(preference_data_key_list) preference_data = {p["id"]: p for p in preference_data_list}
[docs]def includeme(config): """Initialize some preference values""" Preferences.init_from_settings(config.get_settings())