def get_analytics_alerts(discussion, user_id, types, all_users=False): from assembl.semantic.virtuoso_mapping import AssemblQuadStorageManager, AESObfuscator settings = get_config() metrics_server_endpoint = settings.get( "metrics_server_endpoint", "https://discussions.bluenove.com/analytics/accept" ) verify_metrics = False # weird SNI bug on some platforms protocol = "https" if asbool(settings.get("accept_secure_connection", False)) else "http" host = settings.get("public_hostname") if settings.get("public_port", 80) != 80: # TODO: public_secure_port? host += ":" + str(settings.get("public_port")) seed = urandom(8) obfuscator = AESObfuscator(seed) token = permission_token(user_id, discussion.id, [P_READ_PUBLIC_CIF], seed) metrics_requests = [{"metric": "alerts", "types": types}] if user_id != Everyone and not all_users: obfuscated_userid = "local:AgentProfile/" + obfuscator.encrypt(str(user_id)) metrics_requests[0]["users"] = [obfuscated_userid] mapurl = "%s://%s/data/Discussion/%d/jsonld?token=%s" % (protocol, host, discussion.id, token) alerts = requests.post( metrics_server_endpoint, data=dict(mapurl=mapurl, requests=json.dumps(metrics_requests), recency=60), verify=verify_metrics, ) result = AssemblQuadStorageManager.deobfuscate(alerts.text, obfuscator.decrypt) # AgentAccount is a pseudo for AgentProfile result = re.sub(r"local:AgentAccount\\/", r"local:AgentProfile\\/", result) return result
def test_app_no_login_real_policy(request, test_app_no_perm): """A configured Assembl fixture with permissions and no user logged in""" config = testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), ) from ...auth.util import authentication_callback from pyramid.authorization import ACLAuthorizationPolicy from pyramid.path import DottedNameResolver resolver = DottedNameResolver(__package__) auth_policy_name = "assembl.auth.util.UpgradingSessionAuthenticationPolicy" auth_policy = resolver.resolve(auth_policy_name)( callback=authentication_callback) config.set_authorization_policy(ACLAuthorizationPolicy()) config.set_authentication_policy(auth_policy) import transaction # ensure default roles and permissions at startup from ...models import get_session_maker with transaction.manager: session = get_session_maker() from ...lib.migration import bootstrap_db_data bootstrap_db_data(session, False) return test_app_no_perm
def copy_discussion(source_config, dest_config, source_slug, dest_slug, delete=False, debug=False, permissions=None): if (session_maker_is_initialized() and abspath(source_config) == get_config()["__file__"]): # not running from script dest_session = get_session_maker()() dest_metadata = get_metadata() else: dest_metadata, dest_session = engine_from_settings( dest_config, True) dest_tables = dest_metadata.sorted_tables if source_config != dest_config: from assembl.lib.sqla import _session_maker temp = _session_maker assert temp == dest_session source_metadata, source_session = engine_from_settings( source_config, False) source_tables_by_name = { table.name: table.tometadata(source_metadata, source_metadata.schema) for table in dest_tables } _session_maker = dest_session else: source_metadata, source_session = dest_metadata, dest_session try: init_key_for_classes(dest_session) from assembl.models import Discussion discussion = source_session.query(Discussion).filter_by( slug=source_slug).one() assert discussion, "No discussion named " + source_slug permissions = [x.split('+') for x in permissions or ()] for (role, permission) in permissions: assert role in SYSTEM_ROLES assert permission in ASSEMBL_PERMISSIONS existing = dest_session.query(Discussion).filter_by(slug=dest_slug).first() if existing: if delete: print("deleting", dest_slug) with transaction.manager: delete_discussion(dest_session, existing.id) else: print("Discussion", dest_slug), print("already exists! Add -d to delete it.") exit(0) from assembl.models import Role, Permission, DiscussionPermission with dest_session.no_autoflush: copy = clone_discussion( source_session, discussion.id, dest_session, dest_slug) for (role, permission) in permissions: role = dest_session.query(Role).filter_by(name=role).one() permission = dest_session.query(Permission).filter_by( name=permission).one() # assumption: Not already defined. dest_session.add(DiscussionPermission( discussion=copy, role=role, permission=permission)) except Exception: traceback.print_exc() if debug: pdb.post_mortem() capture_exception() return dest_session
def test_app_no_perm(request, base_registry): global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('assembl').location } return TestApp(assembl.main( global_config, nosecurity=True, **get_config()))
def upgrade_semantic_mapping(): from .virtuoso_mapping import AssemblQuadStorageManager from assembl.lib.config import get_config settings = get_config() if settings['sqlalchemy.url'].startswith('virtuoso:'): aqsm = AssemblQuadStorageManager() aqsm.update_all_storages()
def copy_discussion(source_config, dest_config, source_slug, dest_slug, delete=False, debug=False, permissions=None): if (session_maker_is_initialized() and abspath(source_config) == get_config()["__file__"]): # not running from script dest_session = get_session_maker()() dest_metadata = get_metadata() else: dest_metadata, dest_session = engine_from_settings( dest_config, True) dest_tables = dest_metadata.sorted_tables if source_config != dest_config: from assembl.lib.sqla import _session_maker temp = _session_maker assert temp == dest_session source_metadata, source_session = engine_from_settings( source_config, False) source_tables_by_name = { table.name: table.tometadata(source_metadata, source_metadata.schema) for table in dest_tables } _session_maker = dest_session else: source_metadata, source_session = dest_metadata, dest_session try: init_key_for_classes(dest_session) from assembl.models import Discussion discussion = source_session.query(Discussion).filter_by( slug=source_slug).one() assert discussion, "No discussion named " + source_slug permissions = [x.split('+') for x in permissions or ()] for (role, permission) in permissions: assert role in SYSTEM_ROLES assert permission in ASSEMBL_PERMISSIONS existing = dest_session.query(Discussion).filter_by(slug=dest_slug).first() if existing: if delete: print "deleting", dest_slug with transaction.manager: delete_discussion(dest_session, existing.id) else: print "Discussion", dest_slug, print "already exists! Add -d to delete it." exit(0) from assembl.models import Role, Permission, DiscussionPermission with dest_session.no_autoflush: copy = clone_discussion( source_session, discussion.id, dest_session, dest_slug) for (role, permission) in permissions: role = dest_session.query(Role).filter_by(name=role).one() permission = dest_session.query(Permission).filter_by( name=permission).one() # assumption: Not already defined. dest_session.add(DiscussionPermission( discussion=copy, role=role, permission=permission)) except Exception: traceback.print_exc() if debug: pdb.post_mortem() capture_exception() return dest_session
def testing_configurator(request, test_app_no_perm): """The testing configurator""" return testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), )
def get_analytics_alerts(discussion, user_id, types, all_users=False): from assembl.semantic.virtuoso_mapping import (AssemblQuadStorageManager, AESObfuscator) settings = get_config() metrics_server_endpoint = settings.get( 'metrics_server_endpoint', 'https://discussions.bluenove.com/analytics/accept') verify_metrics = False # weird SNI bug on some platforms protocol = 'https' if asbool( settings.get('accept_secure_connection', False)) else 'http' host = settings.get('public_hostname') if settings.get('public_port', 80) != 80: # TODO: public_secure_port? host += ':' + str(settings.get('public_port')) seed = urandom(8) obfuscator = AESObfuscator(seed) token = permission_token(user_id, discussion.id, [P_READ_PUBLIC_CIF], seed) metrics_requests = [{"metric": "alerts", "types": types}] if user_id != Everyone and not all_users: obfuscated_userid = "local:AgentProfile/" + obfuscator.encrypt( str(user_id)) metrics_requests[0]['users'] = [obfuscated_userid] mapurl = '%s://%s/data/Discussion/%d/jsonld?token=%s' % ( protocol, host, discussion.id, token) alerts = requests.post(metrics_server_endpoint, data=dict(mapurl=mapurl, requests=json.dumps(metrics_requests), recency=60), verify=verify_metrics) result = AssemblQuadStorageManager.deobfuscate(alerts.text, obfuscator.decrypt) # AgentAccount is a pseudo for AgentProfile result = re.sub(r'local:AgentAccount\\/', r'local:AgentProfile\\/', result) return result
def fin(): print "finalizer db_default_data" session = db_tables() clear_rows(get_config(), session) transaction.commit() from assembl.models import Locale, LangString Locale.reset_cache() LangString.reset_cache()
def fin(): print("finalizer test_app_no_perm") session = db_tables() with transaction.manager: clear_rows(get_config(), session) from assembl.models import Locale, LangString Locale.reset_cache() LangString.reset_cache()
def test_app_no_perm(request, base_registry, db_tables): global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('assembl').location } app = TestApp(assembl.main( global_config, nosecurity=True, **get_config())) app.PyramidWebTestRequest = PyramidWebTestRequest return app
def test_app_complex_password(request, test_app): """A configured Assembl fixture with permissions and an admin user logged in and strong password requirements enabled""" settings = get_config() settings['minimum_password_complexity'] = 3 settings['password_required_classes'] = None config = testing.setUp(settings=settings) return test_app
def discussion_locales(self): # Ordered list, not empty. # TODO: Guard. Each locale should be 2-letter or posix. # Waiting for utility function. if self.preferred_locales: return self.preferred_locales.split(' ') # Use installation settings otherwise. return config.get_config().get('available_languages', 'fr_CA en_CA').split()
def test_app_no_perm(request, base_registry, db_tables): """A configured Assembl fixture with no permissions""" global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('assembl').location } app = TestApp(assembl.main(global_config, nosecurity=True, **get_config())) app.PyramidWebTestRequest = PyramidWebTestRequest return app
def import_content(self, only_new=True): from assembl.lib.config import get_config from pyramid.settings import asbool assert self.id config = get_config() if asbool(config.get('use_source_reader_for_mail', False)): super(AbstractMailbox, self).import_content(only_new) else: import_mails.delay(self.id, only_new)
def test_results(request): mailer = get_mailer(request) config = get_config() message = Message(subject="test_results", sender=config.get('assembl.admin_email'), recipients=["*****@*****.**"], body=json.dumps(request.POST.dict_of_lists())) mailer.send(message) return Response(body="Thank you!", content_type="text/text")
def discussion_locales(self): # Ordered list, not empty. # TODO: Guard. Each locale should be 2-letter or posix. # Waiting for utility function. if self.preferred_locales: return self.preferred_locales.split(' ') # Use installation settings otherwise. return config.get_config().get( 'available_languages', 'fr_CA en_CA').split()
def test_results(request): mailer = get_mailer(request) config = get_config() message = Message( subject="test_results", sender=config.get('assembl.admin_email'), recipients=["*****@*****.**"], body=json.dumps(request.POST.dict_of_lists())) mailer.send(message) return Response(body="Thank you!", content_type="text/text")
def discussion_locales(self): # Ordered list, not empty. # TODO: Guard. Each locale should be 2-letter or posix. # Waiting for utility function. locales = self.preferences['preferred_locales'] if locales: return locales # Use installation settings otherwise. return [strip_country(l) for l in config.get_config().get( 'available_languages', 'fr en').split()]
def test_app(request, admin_user, test_app_no_perm): config = testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), ) dummy_policy = config.testing_securitypolicy( userid=admin_user.id, permissive=True) config.set_authorization_policy(dummy_policy) config.set_authentication_policy(dummy_policy) return test_app_no_perm
def base_registry(request): from assembl.views.traversal import root_factory from pyramid.config import Configurator from zope.component import getGlobalSiteManager registry = getGlobalSiteManager() config = Configurator(registry) config.setup_registry( settings=get_config(), root_factory=root_factory) configure_tasks(registry, 'assembl') config.add_tween('assembl.tests.pytest_fixtures.zopish_session_tween_factory') return registry
def test_app_no_login(request, admin_user, test_app_no_perm): """A configured Assembl fixture with permissions and no user logged in""" config = testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), ) dummy_policy = config.testing_securitypolicy(userid=None, permissive=False) config.set_authorization_policy(dummy_policy) config.set_authentication_policy(dummy_policy) return test_app_no_perm
def base_registry(request): """A Zope registry that is configured by with the testing.ini""" from assembl.views.traversal import root_factory from pyramid.config import Configurator from zope.component import getGlobalSiteManager registry = getGlobalSiteManager() config = Configurator(registry) config.setup_registry(settings=get_config(), root_factory=root_factory) configure_tasks(registry, 'assembl') config.add_tween('assembl.tests.utils.committing_session_tween_factory') return registry
def base_registry(request): """A Zope registry that is configured by with the testing.ini""" from assembl.views.traversal import root_factory from pyramid.config import Configurator from zope.component import getGlobalSiteManager registry = getGlobalSiteManager() config = Configurator(registry) config.setup_registry( settings=get_config(), root_factory=root_factory) configure_tasks(registry, 'assembl') config.add_tween('assembl.tests.utils.committing_session_tween_factory') return registry
def test_app_no_login(request, admin_user, test_app_no_perm): """A configured Assembl fixture with permissions and no user logged in""" config = testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), ) dummy_policy = config.testing_securitypolicy( userid=None, permissive=False) config.set_authorization_policy(dummy_policy) config.set_authentication_policy(dummy_policy) return test_app_no_perm
def test_app_strong_password(request, test_app): """A configured Assembl fixture with permissions and an admin user logged in and strong password requirements enabled""" settings = get_config() settings['minimum_password_complexity'] = 3 settings['password_required_classes'] = \ {"[a-z]": {"en": "lower-case letter"}, \ "[A-Z]": {"en": "upper-case letter"}, \ "\\d": {"en": "digit"}, \ "\\W": {"en": "special character"}} config = testing.setUp(settings=settings) return test_app
def test_app_no_perm(request, base_registry, db_tables): """A configured IdeaLoom fixture with no permissions""" global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('idealoom').location } config = dict(get_config()) config['nosecurity'] = True app = TestApp(assembl.main(global_config, **config)) app.PyramidWebTestRequest = PyramidWebTestRequest PyramidWebTestRequest._pyramid_app = app.app PyramidWebTestRequest.registry = base_registry return app
def test_app_no_perm(request, base_registry, db_tables): """A configured Assembl fixture with no permissions""" global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('assembl').location } config = dict(get_config()) config['nosecurity'] = True app = TestApp(assembl.main(global_config, **config)) app.PyramidWebTestRequest = PyramidWebTestRequest PyramidWebTestRequest._pyramid_app = app.app PyramidWebTestRequest._registry = base_registry return app
def test_app_participant1(request, participant1_user, test_app_no_perm): """A configured IdeaLoom fixture with permissions and an participant1 user logged in""" config = testing.setUp( registry=test_app_no_perm.app.registry, settings=get_config(), ) dummy_policy = config.testing_securitypolicy( userid=participant1_user.id, permissive=True) config.set_authorization_policy(dummy_policy) config.set_authentication_policy(dummy_policy) return test_app_no_perm
def show_optics_cluster(request): discussion = request.context._instance eps = float(request.GET.get("eps", "0.02")) min_points = int(request.GET.get("min_points", "3")) test_code = request.GET.get("test_code", None) scrambler = None if test_code: from random import Random scrambler = Random() scrambler.seed(get_config().get('session.key') + test_code + discussion.slug) discussion = request.context._instance output = StringIO() from assembl.nlp.clusters import as_html_optics as_html_optics(discussion, output, min_points, eps, scrambler) output.seek(0) return Response(body_file=output, content_type='text/html')
def create_dictionaries(discussion_id=None): db = Discussion.default_db by_main_lang = defaultdict(list) default_locales = get_config().get( 'available_languages', 'fr_CA en_CA').split() only_for_lang = None for d_id, locales in db.query( Discussion.id, Discussion.preferred_locales).all(): locales = locales.split() if locales else default_locales main_lang = locales[0].split('_')[0] by_main_lang[main_lang].append(d_id) if discussion_id == d_id: only_for_lang = main_lang for lang, discussion_ids in by_main_lang.iteritems(): if only_for_lang and only_for_lang != lang: continue dirname = join(nlp_data, lang) if not exists(dirname): makedirs(dirname) corpus_fname = join(dirname, CORPUS_FNAME) if exists(corpus_fname): corpus = IdMmCorpus(corpus_fname) doc_count = db.query(Content).with_polymorphic( Content).options(defer(Content.like_count)).join( Discussion).filter(Discussion.id.in_(discussion_ids) ).count() if corpus.num_docs == doc_count: if only_for_lang: return corpus continue tokenizer = Tokenizer(lang) bowizer = BOWizer(lang, tokenizer, False) posts = db.query(Content).join(Discussion).filter( Discussion.id.in_(discussion_ids)) bowizer.phrases.add_vocab(( tokenizer.tokenize_post(post) for post in posts)) bowizer.dictionary.add_documents(( bowizer.phrases[tokenizer.tokenize_post(post)] for post in posts)) IdMmCorpus.serialize(corpus_fname, ( (post.id, bowizer.post_to_bow(post)) for post in posts)) bowizer.save() return IdMmCorpus(corpus_fname)
def mutate(root, args, context, info): discussion_id = context.matchdict['discussion_id'] discussion = models.Discussion.get(discussion_id) cls = models.Document require_cls_permission(CrudPermissions.CREATE, cls, context) allowed_filetypes = aslist( get_config()['attachment_allowed_mime_types']) def is_matched_type(s): for mime in allowed_filetypes: if re.match(mime, s): return True return False uploaded_file = args.get('file') if uploaded_file is not None: # Because the server is on GNU/Linux, os.path.basename will only work # with path using "/". Using ntpath works for both Linux and Windows path filename = ntpath.basename(context.POST[uploaded_file].filename) content = context.POST[uploaded_file].file.read() context.POST[uploaded_file].file.seek(0) filetype = magic.from_buffer(content, mime=True) if not is_matched_type(filetype): error = _('Sorry, this file type is not allowed.') log.warn( "A MIME-TYPE of %s was uploaded. It was not found in allowed_filetypes." % filetype) raise HTTPUnauthorized(context.localizer.translate(error)) mime_type = context.POST[uploaded_file].type document = models.File(discussion=discussion, mime_type=mime_type, title=filename) document.add_file_data(context.POST[uploaded_file].file) discussion.db.add(document) document.db.flush() return UploadDocument(document=document)
def test_app_no_perm(request, base_registry, db_tables): """A configured IdeaLoom fixture with no permissions""" global_config = { '__file__': request.config.getoption('test_settings_file'), 'here': get_distribution('idealoom').location } config = dict(get_config()) config['nosecurity'] = True app = TestApp(assembl.main(global_config, **config)) app.PyramidWebTestRequest = PyramidWebTestRequest PyramidWebTestRequest._pyramid_app = app.app PyramidWebTestRequest.registry = base_registry def fin(): print("finalizer test_app_no_perm") session = db_tables() with transaction.manager: clear_rows(get_config(), session) request.addfinalizer(fin) return app
def empty_db(request, session_factory): """An SQLAlchemy Session Maker fixture with all tables dropped""" session = session_factory() drop_tables(get_config(), session) return session_factory
permission=P_ADMIN_DISC, header=JSON_HEADER, name="settings") @view_config(context=InstanceContext, request_method='PUT', ctx_instance_class=Discussion, permission=P_ADMIN_DISC, header=JSON_HEADER, name="settings") def discussion_settings_put(request): request.context._instance.settings_json = request.json_body return HTTPOk() dogpile_fname = join(dirname(dirname(dirname(dirname(__file__)))), get_config().get('dogpile_cache.arguments.filename')) discussion_jsonld_cache = get_region('discussion_jsonld', **{"arguments.filename": dogpile_fname}) userprivate_jsonld_cache = get_region('userprivate_jsonld', **{"arguments.filename": dogpile_fname}) @discussion_jsonld_cache.cache_on_arguments() def discussion_jsonld(discussion_id): from assembl.semantic.virtuoso_mapping import AssemblQuadStorageManager aqsm = AssemblQuadStorageManager() return aqsm.as_jsonld(discussion_id) @userprivate_jsonld_cache.cache_on_arguments()
def fin(): print("finalizer db_tables") session = empty_db() drop_tables(get_config(), session) session.commit()
name="settings", ) @view_config( context=InstanceContext, request_method="PUT", ctx_instance_class=Discussion, permission=P_ADMIN_DISC, header=JSON_HEADER, name="settings", ) def discussion_settings_put(request): request.context._instance.settings_json = request.json_body return HTTPOk() dogpile_fname = join(dirname(dirname(dirname(dirname(__file__)))), get_config().get("dogpile_cache.arguments.filename")) discussion_jsonld_cache = get_region("discussion_jsonld", **{"arguments.filename": dogpile_fname}) userprivate_jsonld_cache = get_region("userprivate_jsonld", **{"arguments.filename": dogpile_fname}) @discussion_jsonld_cache.cache_on_arguments() def discussion_jsonld(discussion_id): from assembl.semantic.virtuoso_mapping import AssemblQuadStorageManager aqsm = AssemblQuadStorageManager() return aqsm.as_jsonld(discussion_id) @userprivate_jsonld_cache.cache_on_arguments() def userprivate_jsonld(discussion_id):
def get_discussion_locales(self): # TODO: Notion of active locales per discussion. # Use installation settings for now. # Ordered list, not empty. from assembl.lib.config import get_config return get_config().get('available_languages', 'fr en').split()
def fin(): print "finalizer db_default_data" session = db_tables() clear_rows(get_config(), session) transaction.commit()
def empty_db(request, session_factory): session = session_factory() drop_tables(get_config(), session) return session_factory
class Preferences(MutableMapping, Base, NamedClassMixin): """ Cascading preferences """ __metaclass__ = DeclarativeAbstractMeta __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)) @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 not self.cascade_preferences: return self.local_values_json values = self.cascade_preferences.values_json values.update(self.local_values_json) 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 = unicode(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) # 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.iteritems() } 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.iteritems() } 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": # Will raise a JSONDecodeError if not a valid JSON json.loads(value) else: assert isinstance(value, (str, unicode)), "Not a string" if data_type in ("string", "text"): pass elif data_type == "scalar": assert value in pref_data.get( "scalar_values", ()), ("value not allowed: " + value) elif data_type == "url": condition = False parsed_val = urlparse(value) val = parsed_val.netloc while not condition: # Whilst not an address, requested feature if value in ("*", ): condition = True break elif not bool(parsed_val.scheme): # Must have a scheme, as defined a definition of a URI break elif not val.strip(): # No empty strings allowed break elif is_valid_ipv4_address(val): condition = True break elif is_valid_ipv6_address(val): condition = True break else: # Must be a regular URL then. TODO: Check that the location has a DNS record condition = True break assert condition, "Not a valid URL. Must follow the specification of a URI." 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 == "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 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, aliases, context, permissions, user_id, duplicate_handling=None, jsonld=None): for key, value in json.iteritems(): if key == '@id': if value != self.uri(): raise RuntimeError("Wrong id") else: self[key] = value return self def __hash__(self): return Base.__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: return merge_json(base, patch) else: 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(create=P_SYSADMIN, update=P_ADMIN_DISC) # 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|address|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() ] }, # Whether the discussion uses the new React landing page { "id": "landing_page", "value_type": "bool", "name": _("Use landing page"), "description": _("Are users directed to the landing page and phases at login, or diretly to the debate" ), "allow_user_override": None, "default": False, }, # 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") }, "description": _("Translation service"), "allow_user_override": None, "modification_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" }, # Parameters for frontend settings, for experimentation purposes. # What is put there should become separate parameters for typechecking { "id": "extra_json", "name": _("Extra JSON parameters"), "value_type": "json", "description": _("Parameters for quick frontend settings"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": {} }, # Discussion terms of use { "id": "terms_of_use_url", "name": _("Terms of use URL"), "value_type": "url", "description": _("URL of a document presenting terms of use for the discussion"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": None }, # Discussion Video { "id": "video_url", "name": _("Video URL"), "value_type": "url", "description": _("URL of a video presenting the discussion"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": None }, # Title in the tab { "id": "tab_title", "name": _("Tab title"), "value_type": "string", "description": _("Title which appears on the tab, by default assembl"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": "" }, # Discussion Video description { "id": "video_description", "name": _("Video description"), "value_type": "dict_of_locale_to_text", "description": _("Description of the video presenting the discussion"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": None }, # 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 }, # Public or private social Sharing { "id": "private_social_sharing", "name": _("Private social sharing"), "value_type": "bool", "description": _("Publicizing or privatizing social sharing"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": True, }, # Show generic error message { "id": "generic_errors", "name": _("Generic Errors"), "value_type": "bool", "description": _("Display a generic error message."), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": config.get_config().get('assembl.generic_errors'), }, # Extra data from social fields to put in CSV reports { "id": "extra_csv_data", "name": _("Extra data for CSV reports"), "value_type": "dict_of_string_to_langstr", # "scalar_values": {value: "label"}, "description": _("data taken from social_auth's extra_data to add to CSV reports" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, "backend_validator_function": "assembl.models.social_data_extraction.validate_json_paths", "item_default": { "": { "en": "" } }, "default": {} # for development }, # 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 }, { "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 }, # What are (ordered) identifiers of columns in multi-column views? { "id": "default_column_identifiers", "name": _("Ids of columns in column view"), "value_type": "list_of_string", "description": _("What are (ordered) identifiers of columns in multi-column views?" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "item_default": "", "default": ["positive", "negative"], }, # What are default theme colors of columns in multi-column view { "id": "default_column_colors", "name": _("Default colors of columns in column view"), "value_type": "dict_of_string_to_string", "description": _("What are (default) theme colors of columns in multi-column views?" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": { "positive": "green", "negative": "red" }, }, # What are (default) names of columns in multi-column views, in each discussion language? { "id": "default_column_names", "name": _("Names of columns in column view"), "value_type": "dict_of_string_to_langstr", "description": _("What are (default) names of columns in multi-column views, in each discussion language?" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": { "negative": { "en": "Negative", "fr": "Négatif" }, "positive": { "en": "Positive", "fr": "Positif" } }, "item_default": { "": { "en": "" } }, }, # The specification of the default permissions for a discussion { "id": "default_permissions", "name": _("Default permissions"), "value_type": "dict_of_role_to_list_of_permission", "show_in_preferences": False, "description": _("The basic permissions for a new 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_MY_POST, P_DELETE_POST, P_DISC_STATS, P_EDIT_EXTRACT, P_EDIT_IDEA, P_EDIT_MY_EXTRACT, P_EDIT_MY_POST, P_EDIT_POST, P_EDIT_SYNTHESIS, P_EXPORT_EXTERNAL_SOURCE, P_MANAGE_RESOURCE, P_MODERATE, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_SEND_SYNTHESIS, P_VOTE, ], R_CATCHER: [ P_ADD_EXTRACT, P_ADD_IDEA, P_ADD_POST, P_DELETE_MY_POST, P_EDIT_EXTRACT, P_EDIT_IDEA, P_EDIT_MY_EXTRACT, P_EDIT_MY_POST, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_VOTE, ], R_MODERATOR: [ P_ADD_EXTRACT, P_ADD_IDEA, P_ADD_POST, P_DELETE_MY_POST, P_DELETE_POST, P_DISC_STATS, P_EDIT_EXTRACT, P_EDIT_IDEA, P_EDIT_MY_EXTRACT, P_EDIT_MY_POST, P_EDIT_POST, P_EDIT_SYNTHESIS, P_EXPORT_EXTERNAL_SOURCE, P_MANAGE_RESOURCE, P_MODERATE, P_OVERRIDE_SOCIAL_AUTOLOGIN, P_SEND_SYNTHESIS, P_VOTE, ], R_PARTICIPANT: [ P_ADD_POST, P_DELETE_MY_POST, P_EDIT_MY_POST, P_VOTE, ], Authenticated: [ P_SELF_REGISTER, ], Everyone: [ P_READ, P_READ_PUBLIC_CIF, ], }, }, # 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": "" }, # Allow whitelist to be applied to SSO login process { "id": "whitelist_on_authentication_backend", "name": _("Whitelist on authentication service"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _("Allow white list to be applied to the authentication service chosen" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": False }, # Allow whitelist to be applied to the regular login process { "id": "whitelist_on_register", "name": _("Whitelist on standard registration"), "value_type": "bool", # "scalar_values": {value: "label"}, "description": _("Allow white list to be applied to the default login process"), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": True }, # A discussion administrator, if different from the server administrator { "id": "discussion_administrators", "name": _("Discussion administrators"), "value_type": "list_of_email", # "scalar_values": {value: "label"}, "description": _("A list of discussion administrators, if different from the server administrator" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, # "frontend_validator_function": func_name...?, # "backend_validator_function": func_name...?, "default": False }, # 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 }, # 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... }, # Custom HTML code that will be integrated on the landing page of the debate, right after the <body> tag { "id": "custom_html_code_landing_page", "name": _("Custom HTML code on the landing page"), "value_type": "text", "show_in_preferences": True, "description": _("Custom HTML code that will be integrated on the landing page of the debate, right after the <body> tag" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": None }, # Custom HTML code that will be integrated on the user registration page of the debate, right after the <body> tag { "id": "custom_html_code_user_registration_page", "name": _("Custom HTML code on the user registration page"), "value_type": "text", "show_in_preferences": True, "description": _("Custom HTML code that will be integrated on the user registration page of the debate, right after the <body> tag" ), "allow_user_override": None, "modification_permission": P_ADMIN_DISC, "default": None }, # Harvesting translation { "id": "harvesting_translation", "name": _("Harvesting translation"), "value_type": "dict_of_string_to_string", "show_in_preferences": True, "description": _("Harvesting translation"), "allow_user_override": P_READ, "default": None }, # Valid CORS paths { "id": "graphql_valid_cors", "name": _("Valid CORS paths for GraphQL API calls"), "value_type": "list_of_url", "show_in_preferences": True, "description": _("A list of valid domain names or IP addresses that are allowed to make CORS api calls to the GraphQL API" ), "allow_user_override": False, "default": [], "item_default": "" }, ] # 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}
return request.context._instance.settings_json @view_config(context=InstanceContext, request_method='PATCH', ctx_instance_class=Discussion, permission=P_ADMIN_DISC, header=JSON_HEADER, name="settings") @view_config(context=InstanceContext, request_method='PUT', ctx_instance_class=Discussion, permission=P_ADMIN_DISC, header=JSON_HEADER, name="settings") def discussion_settings_put(request): request.context._instance.settings_json = request.json_body return HTTPOk() dogpile_fname = join( dirname(dirname(dirname(dirname(__file__)))), get_config().get('dogpile_cache.arguments.filename')) discussion_jsonld_cache = get_region( 'discussion_jsonld', **{"arguments.filename": dogpile_fname}) userprivate_jsonld_cache = get_region( 'userprivate_jsonld', **{"arguments.filename": dogpile_fname}) @discussion_jsonld_cache.cache_on_arguments() def discussion_jsonld(discussion_id): from assembl.semantic.virtuoso_mapping import AssemblQuadStorageManager aqsm = AssemblQuadStorageManager() return aqsm.as_jsonld(discussion_id) @userprivate_jsonld_cache.cache_on_arguments()
def fin(): print "finalizer db_tables" session = empty_db() drop_tables(get_config(), session) transaction.commit()