From 53fea6a000cfb334f922af8e1c21b7fde91b1a05 Mon Sep 17 00:00:00 2001
From: Benjamin Dauvergne <bdauvergne@entrouvert.com>
Date: Tue, 3 Jun 2014 15:52:15 +0200
Subject: [PATCH 3/3] add a model representation for expression attribute
 sources

---
 authentic2/admin.py                                |    5 +
 authentic2/dashboard.py                            |    6 +
 .../0017_auto__add_expressionattributesource.py    |  138 ++++++++++++++++++++
 authentic2/models.py                               |   42 +++++-
 .../functionattributegenerator/change_form.html    |    9 ++
 5 files changed, 198 insertions(+), 2 deletions(-)
 create mode 100644 authentic2/migrations/0017_auto__add_expressionattributesource.py
 create mode 100644 authentic2/templates/admin/authentic2/functionattributegenerator/change_form.html

diff --git a/authentic2/admin.py b/authentic2/admin.py
index f29d487..4836b93 100644
--- a/authentic2/admin.py
+++ b/authentic2/admin.py
@@ -196,5 +196,10 @@ class A2GroupAdmin(GroupAdmin):
     form = forms.GroupAdminForm
 
 
+class ExpressionAttributeSourceAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(models.ExpressionAttributeSource, ExpressionAttributeSourceAdmin)
+
 admin.site.unregister(Group)
 admin.site.register(Group, A2GroupAdmin)
diff --git a/authentic2/dashboard.py b/authentic2/dashboard.py
index 275522b..58aaae5 100644
--- a/authentic2/dashboard.py
+++ b/authentic2/dashboard.py
@@ -50,6 +50,12 @@ class CustomIndexDashboard(Dashboard):
                 'authentic2.models.Attribute'),
         ))
         self.children.append(modules.ModelList(
+            _('Attribute sources'),
+            models=(
+                'authentic2.models.ExpressionAttributeSource',
+            ),
+        ))
+        self.children.append(modules.ModelList(
             _('SAML2'),
             models=(
                 'authentic2.saml.models.LibertyProvider',
diff --git a/authentic2/migrations/0017_auto__add_expressionattributesource.py b/authentic2/migrations/0017_auto__add_expressionattributesource.py
new file mode 100644
index 0000000..14aad3c
--- /dev/null
+++ b/authentic2/migrations/0017_auto__add_expressionattributesource.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'ExpressionAttributeSource'
+        db.create_table(u'authentic2_expressionattributesource', (
+            (u'attributesource_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['authentic2.AttributeSource'], unique=True, primary_key=True)),
+            ('condition', self.gf('django.db.models.fields.TextField')(blank=True)),
+            ('expression', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal(u'authentic2', ['ExpressionAttributeSource'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'ExpressionAttributeSource'
+        db.delete_table(u'authentic2_expressionattributesource')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+        },
+        u'authentic2.attribute': {
+            'Meta': {'object_name': 'Attribute'},
+            'asked_on_registration': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'kind': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+            'label': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}),
+            'multiple': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '256'}),
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user_editable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user_visible': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        u'authentic2.attributesource': {
+            'Meta': {'object_name': 'AttributeSource'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'authentic2.attributevalue': {
+            'Meta': {'object_name': 'AttributeValue'},
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['authentic2.Attribute']"}),
+            'content': ('django.db.models.fields.TextField', [], {}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        u'authentic2.authenticationevent': {
+            'Meta': {'object_name': 'AuthenticationEvent'},
+            'how': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'nonce': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'when': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'who': ('django.db.models.fields.CharField', [], {'max_length': '80'})
+        },
+        u'authentic2.deleteduser': {
+            'Meta': {'object_name': 'DeletedUser'},
+            'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+        },
+        u'authentic2.expressionattributesource': {
+            'Meta': {'object_name': 'ExpressionAttributeSource', '_ormbases': [u'authentic2.AttributeSource']},
+            u'attributesource_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['authentic2.AttributeSource']", 'unique': 'True', 'primary_key': 'True'}),
+            'condition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'expression': ('django.db.models.fields.TextField', [], {})
+        },
+        u'authentic2.federatedid': {
+            'Meta': {'object_name': 'FederatedId'},
+            'about': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'id_format': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'id_value': ('django.db.models.fields.TextField', [], {}),
+            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'service': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'authentic2.logouturl': {
+            'Meta': {'object_name': 'LogoutUrl'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'logout_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'logout_use_iframe': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'logout_use_iframe_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '300'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        u'authentic2.userexternalid': {
+            'Meta': {'object_name': 'UserExternalId'},
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'external_id': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'source': ('django.db.models.fields.URLField', [], {'max_length': '256'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['authentic2']
\ No newline at end of file
diff --git a/authentic2/models.py b/authentic2/models.py
index 721657b..51cc611 100644
--- a/authentic2/models.py
+++ b/authentic2/models.py
@@ -2,7 +2,7 @@ import warnings
 import re
 import urlparse
 
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.utils import timezone
 from django.utils.http import urlquote
 from django.conf import settings
@@ -23,7 +23,7 @@ from model_utils.managers import QueryManager, InheritanceManager
 from safe_expression import SafeExpression, InvalidExpression
 
 
-from . import managers, plugins, attribute_kinds
+from . import managers, plugins, attribute_kinds, validators as a2_validators
 
 plugins.init()
 
@@ -391,6 +391,44 @@ class AttributeSource(models.Model):
         verbose_name = _('attribute source')
         verbose_name_plural = _('attribute sources')
 
+class ExpressionAttributeSource(AttributeSource):
+    condition = models.TextField(verbose_name=_('condition expression'),
+            help_text=_('it must be a valid python expression'), blank=True,
+            validators=[a2_validators.safe_python_expression])
+    expression = models.TextField(verbose_name=_('value expression'),
+            help_text=_('it must be a valid python expression'),
+            validators=[a2_validators.safe_python_expression])
+
+    def clean(self):
+        '''Validate expressions'''
+        if self.condition:
+            try:
+                SafeExpression(self.condition).validate(raises=True)
+            except InvalidExpression, e:
+                raise ValidationError(_('invalid condition: %s') % e)
+        try:
+            SafeExpression(self.expression).validate(raises=True)
+        except InvalidExpression, e:
+            raise ValidationError(_('invalid condition: %s') % e)
+
+    def get_definition(self, ctx={}):
+        dependencies = set()
+        if self.condition:
+            dependencies.update(SafeExpression(self.condition).names())
+        dependencies.update(SafeExpression(self.expression).names())
+
+        definition = {
+                'name': self.name,
+                'label': self.description,
+                'dependencies': dependencies,
+                'condition': self.condition,
+                'expression': self.expression,
+        }
+        return definition
+
+    class Meta:
+        verbose_name = _('expression')
+        verbose_name_plural = _('expressions')
 
 class ModelWithAttributeSources(models.Model):
     attribute_sources = models.ManyToManyField(AttributeSource, blank=True)
diff --git a/authentic2/templates/admin/authentic2/functionattributegenerator/change_form.html b/authentic2/templates/admin/authentic2/functionattributegenerator/change_form.html
new file mode 100644
index 0000000..e27ff5b
--- /dev/null
+++ b/authentic2/templates/admin/authentic2/functionattributegenerator/change_form.html
@@ -0,0 +1,9 @@
+{% extends "admin/change_form.html" %}
+
+{% block after_related_objects %}
+{% if original.id %}
+  <pre>
+    {{ original.get_definition|pprint }}
+  </pre>
+{% endif %}
+{% endblock %}
-- 
1.7.10.4

