From a0b47e85b5e4906bacd650e41c9d98825d782015 Mon Sep 17 00:00:00 2001
From: Benjamin Dauvergne <bdauvergne@entrouvert.com>
Date: Tue, 3 Jun 2014 15:51:50 +0200
Subject: [PATCH 2/3] attributes_ng/sources: add new expression source

You can define a python expression and a condition (also a python expression),
the defined attribute is created if the condition match, its value given
by the expression. Dependencies are automatically computed
from the expressions. It depends upon the library python-safe-expression.
---
 authentic2/app_settings.py                     |    1 +
 authentic2/attributes_ng/sources/expression.py |   72 ++++++++++++++++++++++++
 authentic2/validators.py                       |    8 +++
 requirements.txt                               |    1 +
 setup.py                                       |    1 +
 5 files changed, 83 insertions(+)
 create mode 100644 authentic2/attributes_ng/sources/expression.py

diff --git a/authentic2/app_settings.py b/authentic2/app_settings.py
index c2c4bc5..c0c9fa9 100644
--- a/authentic2/app_settings.py
+++ b/authentic2/app_settings.py
@@ -76,6 +76,7 @@ default_settings = dict(
                  'authentic2.attributes_ng.sources.function',
                  'authentic2.attributes_ng.sources.django_user',
                  'authentic2.attributes_ng.sources.ldap',
+                 'authentic2.attributes_ng.sources.expression',
                  ),
         definition='List of attribute backend classes or modules',
     ),
diff --git a/authentic2/attributes_ng/sources/expression.py b/authentic2/attributes_ng/sources/expression.py
new file mode 100644
index 0000000..eeca38b
--- /dev/null
+++ b/authentic2/attributes_ng/sources/expression.py
@@ -0,0 +1,72 @@
+import itertools
+
+from django.core.exceptions import ImproperlyConfigured
+
+from safe_expression import SafeExpression, InvalidExpression
+
+from ...decorators import to_list
+
+AUTHORIZED_KEYS = set(('name', 'label', 'condition', 'expression'))
+
+REQUIRED_KEYS = set(('name', 'expression'))
+
+UNEXPECTED_KEYS_ERROR = \
+        '{0}: unexpected key(s) {1} in configuration'
+MISSING_KEYS_ERROR = \
+        '{0}: missing key(s) {1} in configuration'
+BAD_CONFIG_ERROR = \
+        '{0}: expression attribute source must contain a name and an expression'
+NOT_CALLABLE_ERROR = \
+        '{0}: function attribute must be callable'
+
+def config_error(fmt, *args):
+    raise ImproperlyConfigured(fmt.format(__name__, *args))
+
+@to_list
+def get_instances(ctx):
+    '''
+    Retrieve instances from settings
+    '''
+    from django.conf import settings
+    all_sources = itertools.chain(
+            getattr(settings, 'ATTRIBUTE_SOURCES', []),
+            ctx.get('__attribute_sources__', []))
+    for kind, d in all_sources:
+        if kind != 'expression':
+            continue
+        if not d.get('__validated'):
+            keys = set(d.keys())
+            if not keys <= AUTHORIZED_KEYS:
+                unexpected = keys - AUTHORIZED_KEYS
+                config_error(UNEXPECTED_KEYS_ERROR, unexpected)
+            if not REQUIRED_KEYS <= keys:
+                missing = REQUIRED_KEYS - keys
+                config_error(MISSING_KEYS_ERROR, missing)
+            d['__dependencies'] = set()
+            if 'condition' in d:
+                d['__condition'] = SafeExpression(d['condition'])
+                try:
+                    d['__condition'].validate(raises=True)
+                except InvalidExpression, e:
+                    raise ImproperlyConfigured('invalid condition expression', e)
+                d['__dependencies'].update(d['__condition'].names())
+            d['__expression'] = SafeExpression(d['expression'])
+            try:
+                d['__expression'].validate(raises=True)
+            except InvalidExpression, e:
+                raise ImproperlyConfigured('invalid expression', e)
+            d['__dependencies'].update(d['__expression'].names())
+        d['__validated'] = True
+        yield d
+
+def get_attribute_names(instance, ctx):
+    name = instance['name']
+    return ((name, instance.get('label', name)),)
+
+def get_dependencies(instance, ctx):
+    return instance['__dependencies']
+
+def get_attributes(instance, ctx):
+    if '__condition' in instance and not instance['__condition'](**ctx):
+        return {}
+    return {instance['name']: instance['__expression'](**ctx)}
diff --git a/authentic2/validators.py b/authentic2/validators.py
index e5ff678..5fe6630 100644
--- a/authentic2/validators.py
+++ b/authentic2/validators.py
@@ -99,3 +99,11 @@ def validate_password(password):
         raise ValidationError(_('password must contain characters '
             'from at least %d classes amon: lowercase letters, '
             'uppercase letters, digits, and punctuations') % min_class_count)
+
+from safe_expression import SafeExpression, InvalidExpression
+
+def safe_python_expression(value):
+    try:
+        SafeExpression(value).validate(raises=True)
+    except InvalidExpression, e:
+        raise ValidationError(e.args[0])
diff --git a/requirements.txt b/requirements.txt
index 9c44b4e..92f7c44 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,4 @@ django-admin-tools>=0.5.1
 dnspython
 django-select2
 django-tables2
+safe-expression
diff --git a/setup.py b/setup.py
index 2f8f2c6..527d971 100755
--- a/setup.py
+++ b/setup.py
@@ -123,6 +123,7 @@ setup(name="authentic2",
         'dnspython',
         'django-select2',
         'django-tables2',
+        'safe-expression',
       ],
       zip_safe=False,
       classifiers=[
-- 
1.7.10.4

